diff --git a/Duplicati/Server/Database/Backup.cs b/Duplicati.Library.RestAPI/Database/Backup.cs similarity index 97% rename from Duplicati/Server/Database/Backup.cs rename to Duplicati.Library.RestAPI/Database/Backup.cs index 117253942..f5221817e 100644 --- a/Duplicati/Server/Database/Backup.cs +++ b/Duplicati.Library.RestAPI/Database/Backup.cs @@ -1,150 +1,150 @@ -// Copyright (C) 2015, The Duplicati Team - -// http://www.duplicati.com, info@duplicati.com -// -// This library is free software; you can redistribute it and/or modify -// it under the terms of the GNU Lesser General Public License as -// published by the Free Software Foundation; either version 2.1 of the -// License, or (at your option) any later version. -// -// This library is distributed in the hope that it will be useful, but -// WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU -// Lesser General Public License for more details. -// -// You should have received a copy of the GNU Lesser General Public -// License along with this library; if not, write to the Free Software -// Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA -using System; -using Duplicati.Server.Serialization.Interface; -using System.Collections.Generic; -using System.Collections.Specialized; -using System.Linq; - -namespace Duplicati.Server.Database -{ - public class Backup : IBackup - { - // Sensitive information that may be stored in TargetUrl - private readonly string[] UrlPasswords = { - "authid", - "auth-password", - "sia-password", - "tardigrade-secret", - "tardigrade-shared-access", - }; - - // Sensitive information that may be stored in Settings - private readonly string[] SettingPasswords = { - "passphrase", - "--authid", - "--send-mail-password", - "--send-xmpp-password", - }; - - public Backup() - { - this.ID = null; - } - - internal void LoadChildren(Connection con) - { - if (this.IsTemporary) - { - this.Sources = new string[0]; - this.Settings = new ISetting[0]; - this.Filters = new IFilter[0]; - this.Metadata = new Dictionary(); - } - else - { - var id = long.Parse(this.ID); - this.Sources = con.GetSources(id); - this.Settings = con.GetSettings(id); - this.Filters = con.GetFilters(id); - this.Metadata = con.GetMetadata(id); - } - } - - /// - /// The backup ID - /// - public string ID { get; set; } - /// - /// The backup name - /// - public string Name { get; set; } - /// - /// The backup description - /// - public string Description { get; set; } - /// - /// The backup tags - /// - public string[] Tags { get; set; } - /// - /// The backup target url - /// - public string TargetURL { get; set; } - /// - /// The path to the local database - /// - public string DBPath { get; internal set; } - - /// - /// The backup source folders and files - /// - public string[] Sources { get; set; } - - /// - /// The backup settings - /// - public ISetting[] Settings { get; set; } - - /// - /// The filters applied to the source files - /// - public IFilter[] Filters { get; set; } - - /// - /// The backup metadata - /// - public IDictionary Metadata { get; set; } - - /// - /// Gets a value indicating if this instance is not persisted to the database - /// - public bool IsTemporary { get { return ID != null && ID.IndexOf("-", StringComparison.Ordinal) > 0; } } - - /// - /// Sanitizes the backup TargetUrl from any fields in the PasswordFields list. - /// - public void SanitizeTargetUrl() - { - var url = new Duplicati.Library.Utility.Uri(this.TargetURL); - NameValueCollection filteredParameters = new NameValueCollection(); - if (url.Query != null) - { - // We cannot use url.QueryParameters since it contains decoded parameter values, which - // breaks assumptions made by the decode_uri function in AppUtils.js. Since we are simply - // removing password parameters, we will leave the parameters as they are in the target URL. - filteredParameters = Library.Utility.Uri.ParseQueryString(url.Query, false); - foreach (string field in this.UrlPasswords) - { - filteredParameters.Remove(field); - } - } - url = url.SetQuery(Duplicati.Library.Utility.Uri.BuildUriQuery(filteredParameters)); - this.TargetURL = url.ToString(); - } - - /// - /// Sanitizes the settings from any fields in the PasswordFields list. - /// - public void SanitizeSettings() - { - this.Settings = this.Settings.Where((setting) => !SettingPasswords.Contains(setting.Name)).ToArray(); - } - } -} - +// Copyright (C) 2015, The Duplicati Team + +// http://www.duplicati.com, info@duplicati.com +// +// This library is free software; you can redistribute it and/or modify +// it under the terms of the GNU Lesser General Public License as +// published by the Free Software Foundation; either version 2.1 of the +// License, or (at your option) any later version. +// +// This library is distributed in the hope that it will be useful, but +// WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +// Lesser General Public License for more details. +// +// You should have received a copy of the GNU Lesser General Public +// License along with this library; if not, write to the Free Software +// Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA +using System; +using Duplicati.Server.Serialization.Interface; +using System.Collections.Generic; +using System.Collections.Specialized; +using System.Linq; + +namespace Duplicati.Server.Database +{ + public class Backup : IBackup + { + // Sensitive information that may be stored in TargetUrl + private readonly string[] UrlPasswords = { + "authid", + "auth-password", + "sia-password", + "tardigrade-secret", + "tardigrade-shared-access", + }; + + // Sensitive information that may be stored in Settings + private readonly string[] SettingPasswords = { + "passphrase", + "--authid", + "--send-mail-password", + "--send-xmpp-password", + }; + + public Backup() + { + this.ID = null; + } + + internal void LoadChildren(Connection con) + { + if (this.IsTemporary) + { + this.Sources = new string[0]; + this.Settings = new ISetting[0]; + this.Filters = new IFilter[0]; + this.Metadata = new Dictionary(); + } + else + { + var id = long.Parse(this.ID); + this.Sources = con.GetSources(id); + this.Settings = con.GetSettings(id); + this.Filters = con.GetFilters(id); + this.Metadata = con.GetMetadata(id); + } + } + + /// + /// The backup ID + /// + public string ID { get; set; } + /// + /// The backup name + /// + public string Name { get; set; } + /// + /// The backup description + /// + public string Description { get; set; } + /// + /// The backup tags + /// + public string[] Tags { get; set; } + /// + /// The backup target url + /// + public string TargetURL { get; set; } + /// + /// The path to the local database + /// + public string DBPath { get; internal set; } + + /// + /// The backup source folders and files + /// + public string[] Sources { get; set; } + + /// + /// The backup settings + /// + public ISetting[] Settings { get; set; } + + /// + /// The filters applied to the source files + /// + public IFilter[] Filters { get; set; } + + /// + /// The backup metadata + /// + public IDictionary Metadata { get; set; } + + /// + /// Gets a value indicating if this instance is not persisted to the database + /// + public bool IsTemporary { get { return ID != null && ID.IndexOf("-", StringComparison.Ordinal) > 0; } } + + /// + /// Sanitizes the backup TargetUrl from any fields in the PasswordFields list. + /// + public void SanitizeTargetUrl() + { + var url = new Duplicati.Library.Utility.Uri(this.TargetURL); + NameValueCollection filteredParameters = new NameValueCollection(); + if (url.Query != null) + { + // We cannot use url.QueryParameters since it contains decoded parameter values, which + // breaks assumptions made by the decode_uri function in AppUtils.js. Since we are simply + // removing password parameters, we will leave the parameters as they are in the target URL. + filteredParameters = Library.Utility.Uri.ParseQueryString(url.Query, false); + foreach (string field in this.UrlPasswords) + { + filteredParameters.Remove(field); + } + } + url = url.SetQuery(Duplicati.Library.Utility.Uri.BuildUriQuery(filteredParameters)); + this.TargetURL = url.ToString(); + } + + /// + /// Sanitizes the settings from any fields in the PasswordFields list. + /// + public void SanitizeSettings() + { + this.Settings = this.Settings.Where((setting) => !SettingPasswords.Contains(setting.Name)).ToArray(); + } + } +} + diff --git a/Duplicati/Server/Database/Connection.cs b/Duplicati.Library.RestAPI/Database/Connection.cs similarity index 94% rename from Duplicati/Server/Database/Connection.cs rename to Duplicati.Library.RestAPI/Database/Connection.cs index ce9ed53da..4fd7b1f90 100644 --- a/Duplicati/Server/Database/Connection.cs +++ b/Duplicati.Library.RestAPI/Database/Connection.cs @@ -1,1261 +1,1262 @@ -// Copyright (C) 2015, The Duplicati Team - -// http://www.duplicati.com, info@duplicati.com -// -// This library is free software; you can redistribute it and/or modify -// it under the terms of the GNU Lesser General Public License as -// published by the Free Software Foundation; either version 2.1 of the -// License, or (at your option) any later version. -// -// This library is distributed in the hope that it will be useful, but -// WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU -// Lesser General Public License for more details. -// -// You should have received a copy of the GNU Lesser General Public -// License along with this library; if not, write to the Free Software -// Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA -using System; -using System.Collections.Generic; -using System.Linq; -using Duplicati.Server.Serialization.Interface; -using System.Text; - -namespace Duplicati.Server.Database -{ - public class Connection : IDisposable - { - private readonly System.Data.IDbConnection m_connection; - private System.Data.IDbCommand m_errorcmd; - public readonly object m_lock = new object(); - public const int ANY_BACKUP_ID = -1; - public const int SERVER_SETTINGS_ID = -2; - private readonly Dictionary m_temporaryBackups = new Dictionary(); - - public Connection(System.Data.IDbConnection connection) - { - m_connection = connection; - m_errorcmd = m_connection.CreateCommand(); - m_errorcmd.CommandText = @"INSERT INTO ""ErrorLog"" (""BackupID"", ""Message"", ""Exception"", ""Timestamp"") VALUES (?,?,?,?)"; - for(var i = 0; i < 4; i++) - m_errorcmd.Parameters.Add(m_errorcmd.CreateParameter()); - - this.ApplicationSettings = new ServerSettings(this); - } - - internal void LogError(string backupid, string message, Exception ex) - { - lock(m_lock) - { - if (!long.TryParse(backupid, out long id)) - id = -1; - ((System.Data.IDbDataParameter)m_errorcmd.Parameters[0]).Value = id; - ((System.Data.IDbDataParameter)m_errorcmd.Parameters[1]).Value = message; - ((System.Data.IDbDataParameter)m_errorcmd.Parameters[2]).Value = ex?.ToString(); - ((System.Data.IDbDataParameter)m_errorcmd.Parameters[3]).Value = Library.Utility.Utility.NormalizeDateTimeToEpochSeconds(DateTime.UtcNow); - m_errorcmd.ExecuteNonQuery(); - } - } - - internal void ExecuteWithCommand(Action f) - { - lock(m_lock) - using(var cmd = m_connection.CreateCommand()) - f(cmd); - } - - internal Serializable.ImportExportStructure PrepareBackupForExport(IBackup backup) - { - var scheduleId = GetScheduleIDsFromTags(new string[] { "ID=" + backup.ID }); - return new Serializable.ImportExportStructure() { - CreatedByVersion = System.Reflection.Assembly.GetExecutingAssembly().GetName().Version.ToString(), - Backup = (Database.Backup)backup, - Schedule = (Database.Schedule)(scheduleId.Any() ? GetSchedule(scheduleId.First()) : null), - DisplayNames = SpecialFolders.GetSourceNames(backup) - }; - } - - public string RegisterTemporaryBackup(IBackup backup) - { - lock(m_lock) - { - if (backup == null) - throw new ArgumentNullException(nameof(backup)); - if (backup.ID != null) - throw new ArgumentException("Backup is already active, cannot make temporary"); - - backup.ID = Guid.NewGuid().ToString("D"); - m_temporaryBackups.Add(backup.ID, (Backup)backup); - return backup.ID; - } - } - - public void UnregisterTemporaryBackup(IBackup backup) - { - lock(m_lock) - m_temporaryBackups.Remove(backup.ID); - } - - public void UpdateTemporaryBackup(IBackup backup) - { - lock(m_lock) - if (m_temporaryBackups.Remove(backup.ID)) - m_temporaryBackups.Add(backup.ID, (Backup)backup); - } - - public IBackup GetTemporaryBackup(string id) - { - if (string.IsNullOrEmpty(id)) - return null; - - lock(m_lock) - { - Backup b; - m_temporaryBackups.TryGetValue(id, out b); - return b; - } - } - - public ServerSettings ApplicationSettings { get; private set; } - - internal IDictionary GetMetadata(long id) - { - lock(m_lock) - return ReadFromDb( - (rd) => new KeyValuePair( - ConvertToString(rd, 0), - ConvertToString(rd, 1) - ), - @"SELECT ""Name"", ""Value"" FROM ""Metadata"" WHERE ""BackupID"" = ? ", id) - .ToDictionary((k) => k.Key, (k) => k.Value); - } - - internal void SetMetadata(IDictionary values, long id, System.Data.IDbTransaction transaction) - { - lock(m_lock) - using(var tr = transaction == null ? m_connection.BeginTransaction() : null) - { - OverwriteAndUpdateDb( - tr, - @"DELETE FROM ""Metadata"" WHERE ""BackupID"" = ?", new object[] { id }, - values ?? new Dictionary(), - @"INSERT INTO ""Metadata"" (""BackupID"", ""Name"", ""Value"") VALUES (?, ?, ?)", - (f) => new object[] { id, f.Key, f.Value } - ); - - if (tr != null) - tr.Commit(); - } - } - - internal IFilter[] GetFilters(long id) - { - lock(m_lock) - return ReadFromDb( - (rd) => (IFilter)new Filter() { - Order = ConvertToInt64(rd, 0), - Include = ConvertToBoolean(rd, 1), - Expression = ConvertToString(rd, 2) ?? "" - }, - @"SELECT ""Order"", ""Include"", ""Expression"" FROM ""Filter"" WHERE ""BackupID"" = ? ORDER BY ""Order"" ", id) - .ToArray(); - } - - internal void SetFilters(IEnumerable values, long id, System.Data.IDbTransaction transaction = null) - { - lock(m_lock) - using(var tr = transaction == null ? m_connection.BeginTransaction() : null) - { - OverwriteAndUpdateDb( - tr, - @"DELETE FROM ""Filter"" WHERE ""BackupID"" = ?", new object[] { id }, - values, - @"INSERT INTO ""Filter"" (""BackupID"", ""Order"", ""Include"", ""Expression"") VALUES (?, ?, ?, ?)", - (f) => new object[] { id, f.Order, f.Include, f.Expression } - ); - - if (tr != null) - tr.Commit(); - } - } - - public ISetting[] GetSettings(long id) - { - lock(m_lock) - return ReadFromDb( - (rd) => (ISetting)new Setting() { - Filter = ConvertToString(rd, 0) ?? "", - Name = ConvertToString(rd, 1) ?? "", - Value = ConvertToString(rd, 2) ?? "" - //TODO: Attach the argument information - }, - @"SELECT ""Filter"", ""Name"", ""Value"" FROM ""Option"" WHERE ""BackupID"" = ?", id) - .ToArray(); - } - - internal void SetSettings(IEnumerable values, long id, System.Data.IDbTransaction transaction = null) - { - lock(m_lock) - using(var tr = transaction == null ? m_connection.BeginTransaction() : null) - { - OverwriteAndUpdateDb( - tr, - @"DELETE FROM ""Option"" WHERE ""BackupID"" = ?", new object[] { id }, - values, - @"INSERT INTO ""Option"" (""BackupID"", ""Filter"", ""Name"", ""Value"") VALUES (?, ?, ?, ?)", - (f) => { - if (Duplicati.Server.WebServer.Server.PASSWORD_PLACEHOLDER.Equals(f.Value)) - throw new Exception("Attempted to save a property with the placeholder password"); - return new object[] { id, f.Filter ?? "", f.Name, f.Value ?? "" }; - } - ); - - if (tr != null) - tr.Commit(); - } - } - - internal string[] GetSources(long id) - { - lock(m_lock) - return ReadFromDb( - (rd) => ConvertToString(rd, 0), - @"SELECT ""Path"" FROM ""Source"" WHERE ""BackupID"" = ?", id) - .ToArray(); - } - - internal void SetSources(IEnumerable values, long id, System.Data.IDbTransaction transaction) - { - lock(m_lock) - using(var tr = transaction == null ? m_connection.BeginTransaction() : null) - { - OverwriteAndUpdateDb( - tr, - @"DELETE FROM ""Source"" WHERE ""BackupID"" = ?", new object[] { id }, - values, - @"INSERT INTO ""Source"" (""BackupID"", ""Path"") VALUES (?, ?)", - (f) => new object[] { id, f } - ); - - if (tr != null) - tr.Commit(); - } - } - - internal long[] GetBackupIDsForTags(string[] tags) - { - if (tags == null || tags.Length == 0) - return new long[0]; - - if (tags.Length == 1 && tags[0].StartsWith("ID=", StringComparison.Ordinal)) - return new long[] { long.Parse(tags[0].Substring("ID=".Length)) }; - - lock(m_lock) - using(var cmd = m_connection.CreateCommand()) - { - var sb = new StringBuilder(); - - foreach(var t in tags) - { - if (sb.Length != 0) - sb.Append(" OR "); - sb.Append(@" ("","" || ""Tags"" || "","" LIKE ""%,"" || ? || "",%"") "); - - var p = cmd.CreateParameter(); - p.Value = t; - cmd.Parameters.Add(p); - } - - cmd.CommandText = @"SELECT ""ID"" FROM ""Backup"" WHERE " + sb; - - return Read(cmd, (rd) => ConvertToInt64(rd, 0)).ToArray(); - } - } - - internal IBackup GetBackup(string id) - { - if (string.IsNullOrWhiteSpace(id)) - throw new ArgumentNullException(nameof(id)); - - return long.TryParse(id, out long lid) ? GetBackup(lid) : GetTemporaryBackup(id); - } - - internal IBackup GetBackup(long id) - { - lock(m_lock) - { - var bk = ReadFromDb( - (rd) => new Backup { - ID = ConvertToInt64(rd, 0).ToString(), - Name = ConvertToString(rd, 1), - Description = ConvertToString(rd, 2), - Tags = (ConvertToString(rd, 3) ?? "").Split(new char[] { ',' }, StringSplitOptions.RemoveEmptyEntries), - TargetURL = ConvertToString(rd, 4), - DBPath = ConvertToString(rd, 5), - }, - @"SELECT ""ID"", ""Name"", ""Description"", ""Tags"", ""TargetURL"", ""DBPath"" FROM ""Backup"" WHERE ID = ?", id) - .FirstOrDefault(); - - if (bk != null) - bk.LoadChildren(this); - - return bk; - } - } - - internal ISchedule GetSchedule(long id) - { - lock(m_lock) - { - var bk = ReadFromDb( - (rd) => new Schedule { - ID = ConvertToInt64(rd, 0), - Tags = (ConvertToString(rd, 1) ?? "").Split(new char[] { ',' }, StringSplitOptions.RemoveEmptyEntries), - Time = ConvertToDateTime(rd, 2), - Repeat = ConvertToString(rd, 3), - LastRun = ConvertToDateTime(rd, 4), - Rule = ConvertToString(rd, 5), - }, - @"SELECT ""ID"", ""Tags"", ""Time"", ""Repeat"", ""LastRun"", ""Rule"" FROM ""Schedule"" WHERE ID = ?", id) - .FirstOrDefault(); - - return bk; - } - } - - internal Boolean IsUnencryptedOrPassphraseStored(long id) - { - lock (m_lock) - { - var usesEncryption = ReadFromDb( - (rd) => ConvertToBoolean(rd, 0), - @"SELECT VALUE != """" FROM ""Option"" WHERE BackupID = ? AND NAME='encryption-module'", id) - .FirstOrDefault(); - - if (!usesEncryption) - { - return true; - } - - return ReadFromDb( - (rd) => ConvertToBoolean(rd, 0), - @"SELECT VALUE != """" FROM ""Option"" WHERE BackupID = ? AND NAME='passphrase'", id) - .FirstOrDefault(); - } - } - - internal long[] GetScheduleIDsFromTags(string[] tags) - { - if (tags == null || tags.Length == 0) - return new long[0]; - - lock(m_lock) - using(var cmd = m_connection.CreateCommand()) - { - var sb = new StringBuilder(); - - foreach(var t in tags) - { - if (sb.Length != 0) - sb.Append(" OR "); - sb.Append(@" ("","" || ""Tags"" || "","" LIKE ""%,"" || ? || "",%"") "); - - var p = cmd.CreateParameter(); - p.Value = t; - cmd.Parameters.Add(p); - } - - cmd.CommandText = @"SELECT ""ID"" FROM ""Schedule"" WHERE " + sb; - - return Read(cmd, (rd) => ConvertToInt64(rd, 0)).ToArray(); - } - } - - public void AddOrUpdateBackupAndSchedule(IBackup item, ISchedule schedule) - { - AddOrUpdateBackup(item, true, schedule); - } - - public string ValidateBackup(IBackup item, ISchedule schedule) - { - if (string.IsNullOrWhiteSpace(item.Name)) - return "Missing a name"; - - if (string.IsNullOrWhiteSpace(item.TargetURL)) - return "Missing a target"; - - if (item.Sources == null || item.Sources.Any(x => string.IsNullOrWhiteSpace(x)) || item.Sources.Length == 0) - return "Invalid source list"; - - var disabled_encryption = false; - var passphrase = string.Empty; - var gpgAsymmetricEncryption = false; - if (item.Settings != null) - { - foreach (var s in item.Settings) - - if (string.Equals(s.Name, "--no-encryption", StringComparison.OrdinalIgnoreCase)) - disabled_encryption = string.IsNullOrWhiteSpace(s.Value) || Library.Utility.Utility.ParseBool(s.Value, false); - else if (string.Equals(s.Name, "passphrase", StringComparison.OrdinalIgnoreCase)) - passphrase = s.Value; - else if (string.Equals(s.Name, "keep-versions", StringComparison.OrdinalIgnoreCase)) - { - int i; - if (!int.TryParse(s.Value, out i) || i <= 0) - return "Retention value must be a positive integer"; - } - else if (string.Equals(s.Name, "keep-time", StringComparison.OrdinalIgnoreCase)) - { - try - { - var ts = Library.Utility.Timeparser.ParseTimeSpan(s.Value); - if (ts <= TimeSpan.FromMinutes(5)) - return "Retention value must be more than 5 minutes"; - } - catch - { - return "Retention value must be a valid timespan"; - } - } - else if (string.Equals(s.Name, "dblock-size", StringComparison.OrdinalIgnoreCase)) - { - try - { - var ds = Library.Utility.Sizeparser.ParseSize(s.Value); - if (ds < 1024 * 1024) - return "DBlock size must be at least 1MB"; - } - catch - { - return "DBlock value must be a valid size string"; - } - } - else if (string.Equals(s.Name, "--blocksize", StringComparison.OrdinalIgnoreCase)) - { - try - { - var ds = Library.Utility.Sizeparser.ParseSize(s.Value); - if (ds < 1024 || ds > int.MaxValue) - return "The blocksize must be at least 1KB"; - } - catch - { - return "The blocksize value must be a valid size string"; - } - } - else if (string.Equals(s.Name, "--prefix", StringComparison.OrdinalIgnoreCase)) - { - if (!string.IsNullOrWhiteSpace(s.Value) && s.Value.Contains("-")) - return "The prefix cannot contain hyphens (-)"; - } - else if (string.Equals(s.Name, "--gpg-encryption-command", StringComparison.OrdinalIgnoreCase)) { - gpgAsymmetricEncryption = string.Equals(s.Value, "--encrypt", StringComparison.OrdinalIgnoreCase); - } - } - - if (!disabled_encryption && !gpgAsymmetricEncryption && string.IsNullOrWhiteSpace(passphrase)) - return "Missing passphrase"; - - if (schedule != null) - { - try - { - var ts = Library.Utility.Timeparser.ParseTimeSpan(schedule.Repeat); - if (ts <= TimeSpan.FromMinutes(5)) - return "Schedule repetition time must be more than 5 minutes"; - } - catch - { - return "Schedule repetition value must be a valid timespan"; - } - - } - - return null; - } - - internal void UpdateBackupDBPath(IBackup item, string path) - { - lock (m_lock) - { - using (var tr = m_connection.BeginTransaction()) - { - using (var cmd = m_connection.CreateCommand()) - { - cmd.Transaction = tr; - cmd.Parameters.Add(cmd.CreateParameter()); - ((System.Data.IDbDataParameter) cmd.Parameters[0]).Value = path; - cmd.Parameters.Add(cmd.CreateParameter()); - ((System.Data.IDbDataParameter) cmd.Parameters[1]).Value = item.ID; - - cmd.CommandText = @"UPDATE ""Backup"" SET ""DBPath""=? WHERE ""ID""=?"; - cmd.ExecuteNonQuery(); - tr.Commit(); - } - } - } - - System.Threading.Interlocked.Increment(ref Program.LastDataUpdateID); - Program.StatusEventNotifyer.SignalNewEvent(); - } - - private void AddOrUpdateBackup(IBackup item, bool updateSchedule, ISchedule schedule) - { - lock(m_lock) - { - bool update = item.ID != null; - if (!update && item.DBPath == null) - { - var folder = Program.DataFolder; - if (!System.IO.Directory.Exists(folder)) - System.IO.Directory.CreateDirectory(folder); - - for(var i = 0; i < 100; i++) - { - var guess = System.IO.Path.Combine(folder, System.IO.Path.ChangeExtension(Duplicati.Library.Main.DatabaseLocator.GenerateRandomName(), ".sqlite")); - if (!System.IO.File.Exists(guess)) - { - ((Backup)item).DBPath = guess; - break; - } - } - - if (item.DBPath == null) - throw new Exception("Unable to generate a unique database file name"); - } - - using(var tr = m_connection.BeginTransaction()) - { - OverwriteAndUpdateDb( - tr, - null, - new object[] { long.Parse(item.ID ?? "-1") }, - new IBackup[] { item }, - update ? - @"UPDATE ""Backup"" SET ""Name""=?, ""Description""=?, ""Tags""=?, ""TargetURL""=? WHERE ""ID""=?" : - @"INSERT INTO ""Backup"" (""Name"", ""Description"", ""Tags"", ""TargetURL"", ""DBPath"") VALUES (?,?,?,?,?)", - (n) => { - - if (n.TargetURL.IndexOf(Duplicati.Server.WebServer.Server.PASSWORD_PLACEHOLDER, StringComparison.Ordinal) >= 0) - throw new Exception("Attempted to save a backup with the password placeholder"); - if (update && long.Parse(n.ID) <= 0) - throw new Exception("Invalid update, cannot update application settings through update method"); - - return new object[] { - n.Name, - n.Description ?? "" , // Description is optional but the column is set to NOT NULL, an additional check is welcome - string.Join(",", n.Tags ?? new string[0]), - n.TargetURL, - update ? item.ID : n.DBPath - }; - }); - - if (!update) - using(var cmd = m_connection.CreateCommand()) - { - cmd.Transaction = tr; - cmd.CommandText = @"SELECT last_insert_rowid();"; - item.ID = ExecuteScalarInt64(cmd).ToString(); - } - - var id = long.Parse(item.ID); - - if (long.Parse(item.ID) <= 0) - throw new Exception("Invalid addition, cannot update application settings through update method"); - - SetSources(item.Sources, id, tr); - SetSettings(item.Settings, id, tr); - SetFilters(item.Filters, id, tr); - SetMetadata(item.Metadata, id, tr); - - if (updateSchedule) - { - var tags = new string[] { "ID=" + item.ID }; - var existing = GetScheduleIDsFromTags(tags); - if (schedule == null && existing.Any()) - DeleteFromDb("Schedule", existing.First(), tr); - else if (schedule != null) - { - if (existing.Any()) - { - var cur = GetSchedule(existing.First()); - cur.AllowedDays = schedule.AllowedDays; - cur.Repeat = schedule.Repeat; - cur.Tags = schedule.Tags; - cur.Time = schedule.Time; - - schedule = cur; - } - else - { - schedule.ID = -1; - } - - schedule.Tags = tags; - AddOrUpdateSchedule(schedule, tr); - } - } - - tr.Commit(); - System.Threading.Interlocked.Increment(ref Program.LastDataUpdateID); - Program.StatusEventNotifyer.SignalNewEvent(); - } - } - } - - internal void AddOrUpdateSchedule(ISchedule item) - { - lock(m_lock) - using(var tr = m_connection.BeginTransaction()) - { - AddOrUpdateSchedule(item, tr); - tr.Commit(); - System.Threading.Interlocked.Increment(ref Program.LastDataUpdateID); - Program.StatusEventNotifyer.SignalNewEvent(); - } - } - - private void AddOrUpdateSchedule(ISchedule item, System.Data.IDbTransaction tr) - { - lock(m_lock) - { - bool update = item.ID >= 0; - OverwriteAndUpdateDb( - tr, - null, - new object[] { item.ID }, - new ISchedule[] { item }, - update ? - @"UPDATE ""Schedule"" SET ""Tags""=?, ""Time""=?, ""Repeat""=?, ""LastRun""=?, ""Rule""=? WHERE ""ID""=?" : - @"INSERT INTO ""Schedule"" (""Tags"", ""Time"", ""Repeat"", ""LastRun"", ""Rule"") VALUES (?,?,?,?,?)", - (n) => new object[] { - string.Join(",", n.Tags), - Library.Utility.Utility.NormalizeDateTimeToEpochSeconds(n.Time), - n.Repeat, - Library.Utility.Utility.NormalizeDateTimeToEpochSeconds(n.LastRun), - n.Rule ?? "", - update ? (object)item.ID : null - }); - - if (!update) - using(var cmd = m_connection.CreateCommand()) - { - cmd.Transaction = tr; - cmd.CommandText = @"SELECT last_insert_rowid();"; - item.ID = ExecuteScalarInt64(cmd); - } - } - } - - public void DeleteBackup(long ID) - { - if (ID < 0) - return; - - lock(m_lock) - { - using(var tr = m_connection.BeginTransaction()) - { - var existing = GetScheduleIDsFromTags(new string[] { "ID=" + ID.ToString() }); - if (existing.Any()) - DeleteFromDb("Schedule", existing.First(), tr); - - DeleteFromDb("ErrorLog", ID, "BackupID", tr); - DeleteFromDb("Filter", ID, "BackupID", tr); - DeleteFromDb("Log", ID, "BackupID", tr); - DeleteFromDb("Metadata", ID, "BackupID", tr); - DeleteFromDb("Option", ID, "BackupID", tr); - DeleteFromDb("Source", ID, "BackupID", tr); - - DeleteFromDb("Backup", ID, tr); - - tr.Commit(); - } - } - - System.Threading.Interlocked.Increment(ref Program.LastDataUpdateID); - Program.StatusEventNotifyer.SignalNewEvent(); - } - - public void DeleteBackup(IBackup backup) - { - if (backup.IsTemporary) - UnregisterTemporaryBackup(backup); - else - DeleteBackup(long.Parse(backup.ID)); - } - - public void DeleteSchedule(long ID) - { - if (ID < 0) - return; - - lock(m_lock) - DeleteFromDb("Schedule", ID); - - System.Threading.Interlocked.Increment(ref Program.LastDataUpdateID); - Program.StatusEventNotifyer.SignalNewEvent(); - } - - public void DeleteSchedule(ISchedule schedule) - { - DeleteSchedule(schedule.ID); - } - - public IBackup[] Backups - { - get - { - lock(m_lock) - { - var lst = ReadFromDb( - (rd) => (IBackup)new Backup() { - ID = ConvertToInt64(rd, 0).ToString(), - Name = ConvertToString(rd, 1), - Description = ConvertToString(rd, 2), - Tags = (ConvertToString(rd, 3) ?? "").Split(new char[] { ',' }, StringSplitOptions.RemoveEmptyEntries), - TargetURL = ConvertToString(rd, 4), - DBPath = ConvertToString(rd, 5), - }, - @"SELECT ""ID"", ""Name"", ""Description"", ""Tags"", ""TargetURL"", ""DBPath"" FROM ""Backup"" ") - .ToArray(); - - foreach(var n in lst) - n.Metadata = GetMetadata(long.Parse(n.ID)); - - - return lst; - } - } - } - - public ISchedule[] Schedules - { - get - { - lock(m_lock) - return ReadFromDb( - (rd) => (ISchedule)new Schedule() { - ID = ConvertToInt64(rd, 0), - Tags = (ConvertToString(rd, 1) ?? "").Split(new char[] { ',' }, StringSplitOptions.RemoveEmptyEntries), - Time = ConvertToDateTime(rd, 2), - Repeat = ConvertToString(rd, 3), - LastRun = ConvertToDateTime(rd, 4), - Rule = ConvertToString(rd, 5), - }, - @"SELECT ""ID"", ""Tags"", ""Time"", ""Repeat"", ""LastRun"", ""Rule"" FROM ""Schedule"" ") - .ToArray(); - } - } - - - public IFilter[] Filters - { - get { return GetFilters(ANY_BACKUP_ID); } - set { SetFilters(value, ANY_BACKUP_ID); } - } - - public ISetting[] Settings - { - get { return GetSettings(ANY_BACKUP_ID); } - set { SetSettings(value, ANY_BACKUP_ID); } - } - - public INotification[] GetNotifications() - { - lock(m_lock) - return ReadFromDb(null).Cast().ToArray(); - } - - public bool DismissNotification(long id) - { - lock(m_lock) - { - var notifications = GetNotifications(); - var cur = notifications.FirstOrDefault(x => x.ID == id); - if (cur == null) - return false; - - DeleteFromDb(typeof(Notification).Name, id); - Program.DataConnection.ApplicationSettings.UnackedError = notifications.Any(x => x.ID != id && x.Type == Duplicati.Server.Serialization.NotificationType.Error); - Program.DataConnection.ApplicationSettings.UnackedWarning = notifications.Any(x => x.ID != id && x.Type == Duplicati.Server.Serialization.NotificationType.Warning); - } - - System.Threading.Interlocked.Increment(ref Program.LastNotificationUpdateID); - Program.StatusEventNotifyer.SignalNewEvent(); - - return true; - } - - public void RegisterNotification(Serialization.NotificationType type, string title, string message, Exception ex, string backupid, string action, string logid, string messageid, string logtag, Func conflicthandler) - { - lock(m_lock) - { - var notification = new Notification() - { - ID = -1, - Type = type, - Title = title, - Message = message, - Exception = ex == null ? "" : ex.ToString(), - BackupID = backupid, - Action = action ?? "", - Timestamp = DateTime.UtcNow, - LogEntryID = logid, - MessageID = messageid, - MessageLogTag = logtag - }; - - var conflictResult = conflicthandler(notification, GetNotifications()); - if (conflictResult == null) - return; - - if (conflictResult != notification) - DeleteFromDb(typeof(Notification).Name, conflictResult.ID); - - OverwriteAndUpdateDb(null, null, null, new Notification[] { notification }, false); - - if (type == Duplicati.Server.Serialization.NotificationType.Error) - Program.DataConnection.ApplicationSettings.UnackedError = true; - else if (type == Duplicati.Server.Serialization.NotificationType.Warning) - Program.DataConnection.ApplicationSettings.UnackedWarning = true; - } - - System.Threading.Interlocked.Increment(ref Program.LastNotificationUpdateID); - Program.StatusEventNotifyer.SignalNewEvent(); - } - - //Workaround to clean up the database after invalid settings update - public void FixInvalidBackupId() - { - using(var cmd = m_connection.CreateCommand()) - using (var tr = m_connection.BeginTransaction()) - { - cmd.Transaction = tr; - cmd.Parameters.Add(cmd.CreateParameter()); - ((System.Data.IDbDataParameter)cmd.Parameters[0]).Value = -1; - cmd.CommandText = @"DELETE FROM ""Option"" WHERE ""BackupID"" = ?"; - cmd.ExecuteNonQuery(); - cmd.CommandText = @"DELETE FROM ""Metadata"" WHERE ""BackupID"" = ?"; - cmd.ExecuteNonQuery(); - cmd.CommandText = @"DELETE FROM ""Filter"" WHERE ""BackupID"" = ?"; - cmd.ExecuteNonQuery(); - cmd.CommandText = @"DELETE FROM ""Source"" WHERE ""BackupID"" = ?"; - cmd.ExecuteNonQuery(); - - cmd.Parameters.Clear(); - cmd.Parameters.Add(cmd.CreateParameter()); - - ((System.Data.IDbDataParameter)cmd.Parameters[0]).Value = "ID=-1"; - cmd.CommandText = @"DELETE FROM ""Schedule"" WHERE ""Tags"" = ?"; - cmd.ExecuteNonQuery(); - tr.Commit(); - } - - ApplicationSettings.FixedInvalidBackupId = true; - } - - public string[] GetUISettingsSchemes() - { - lock(m_lock) - return ReadFromDb( - (rd) => ConvertToString(rd, 0) ?? "", - @"SELECT DISTINCT ""Scheme"" FROM ""UIStorage""") - .ToArray(); - } - - public IDictionary GetUISettings(string scheme) - { - lock(m_lock) - return ReadFromDb( - (rd) => new KeyValuePair( - ConvertToString(rd, 0) ?? "", - ConvertToString(rd, 1) ?? "" - ), - @"SELECT ""Key"", ""Value"" FROM ""UIStorage"" WHERE ""Scheme"" = ?", - scheme) - .GroupBy(x => x.Key) - .ToDictionary(x => x.Key, x => x.Last().Value); - } - - public void SetUISettings(string scheme, IDictionary values, System.Data.IDbTransaction transaction = null) - { - lock(m_lock) - using(var tr = transaction == null ? m_connection.BeginTransaction() : null) - { - OverwriteAndUpdateDb( - tr, - @"DELETE FROM ""UIStorage"" WHERE ""Scheme"" = ?", new object[] { scheme }, - values, - @"INSERT INTO ""UIStorage"" (""Scheme"", ""Key"", ""Value"") VALUES (?, ?, ?)", - (f) => { - return new object[] { scheme, f.Key ?? "", f.Value ?? "" }; - } - ); - - if (tr != null) - tr.Commit(); - } - } - - public void UpdateUISettings(string scheme, IDictionary values, System.Data.IDbTransaction transaction = null) - { - lock (m_lock) - using (var tr = transaction == null ? m_connection.BeginTransaction() : null) - { - OverwriteAndUpdateDb( - tr, - @"DELETE FROM ""UIStorage"" WHERE ""Scheme"" = ? AND ""Key"" IN (?)", new object[] { scheme, values.Keys }, - values.Where(x => x.Value != null), - @"INSERT INTO ""UIStorage"" (""Scheme"", ""Key"", ""Value"") VALUES (?, ?, ?)", - (f) => - { - return new object[] { scheme, f.Key ?? "", f.Value ?? "" }; - } - ); - - if (tr != null) - tr.Commit(); - } - } - - public TempFile[] GetTempFiles() - { - lock(m_lock) - return ReadFromDb(null).ToArray(); - } - - public void DeleteTempFile(long id) - { - lock(m_lock) - DeleteFromDb(typeof(TempFile).Name, id); - } - - public long RegisterTempFile(string origin, string path, DateTime expires) - { - var tempfile = new TempFile() { - Timestamp = DateTime.Now, - Origin = origin, - Path = path, - Expires = expires - }; - - OverwriteAndUpdateDb(null, null, null, new TempFile[] { tempfile }, false); - - return tempfile.ID; - } - - public void PurgeLogData(DateTime purgeDate) - { - var t = Library.Utility.Utility.NormalizeDateTimeToEpochSeconds(purgeDate); - - using(var tr = m_connection.BeginTransaction()) - using(var cmd = m_connection.CreateCommand()) - { - cmd.Transaction = tr; - cmd.CommandText = @"DELETE FROM ""ErrorLog"" WHERE ""Timestamp"" < ?"; - cmd.Parameters.Add(cmd.CreateParameter()); - ((System.Data.IDataParameter)cmd.Parameters[0]).Value = t; - cmd.ExecuteNonQuery(); - - tr.Commit(); - } - } - - private static DateTime ConvertToDateTime(System.Data.IDataReader rd, int index) - { - var unixTime = ConvertToInt64(rd, index); - return unixTime == 0 ? new DateTime(0) : Library.Utility.Utility.EPOCH.AddSeconds(unixTime); - } - - private static bool ConvertToBoolean(System.Data.IDataReader rd, int index) - { - return ConvertToInt64(rd, index) == 1; - } - - private static string ConvertToString(System.Data.IDataReader rd, int index) - { - var r = rd.GetValue(index); - return r == null || r == DBNull.Value ? null : r.ToString(); - } - - private static long ConvertToInt64(System.Data.IDataReader rd, int index) - { - try - { - if (!rd.IsDBNull(index)) - return rd.GetInt64(index); - } - catch - { - } - - return -1; - } - - private static long ExecuteScalarInt64(System.Data.IDbCommand cmd, long defaultValue = -1) - { - using(var rd = cmd.ExecuteReader()) - return rd.Read() ? ConvertToInt64(rd, 0) : defaultValue; - } - - private static string ExecuteScalarString(System.Data.IDbCommand cmd) - { - using(var rd = cmd.ExecuteReader()) - return rd.Read() ? ConvertToString(rd, 0) : null; - - } - - private object ConvertToEnum(Type enumType, System.Data.IDataReader rd, int index, object @default) - { - try - { - return Enum.Parse(enumType, ConvertToString(rd, index)); - } - catch - { - } - - return @default; - } - - // Overloaded function for legacy functionality - private bool DeleteFromDb(string tablename, long id, System.Data.IDbTransaction transaction = null) - { - return DeleteFromDb(tablename, id, "ID", transaction); - } - - // New function that allows to delete rows from tables with arbitrary identifier values (e.g. ID or BackupID) - private bool DeleteFromDb(string tablename, long id, string identifier, System.Data.IDbTransaction transaction = null) - { - if (transaction == null) - { - using(var tr = m_connection.BeginTransaction()) - { - var r = DeleteFromDb(tablename, id, tr); - tr.Commit(); - return r; - } - } - else - { - using(var cmd = m_connection.CreateCommand()) - { - cmd.Transaction = transaction; - cmd.CommandText = string.Format(@"DELETE FROM ""{0}"" WHERE ""{1}""=?", tablename, identifier); - var p = cmd.CreateParameter(); - p.Value = id; - cmd.Parameters.Add(p); - - var r = cmd.ExecuteNonQuery(); - // Roll back the transaction if more than 1 ID was deleted. Multiple "BackupID" rows being deleted isn't a problem. - if (identifier == "ID" && r > 1) - throw new Exception(string.Format("Too many records attempted deleted from table {0} for id {1}: {2}", tablename, id, r)); - return r == 1; - } - } - } - - private static IEnumerable Read(System.Data.IDbCommand cmd, Func f) - { - using(var rd = cmd.ExecuteReader()) - while(rd.Read()) - yield return f(rd); - } - - private static IEnumerable Read(System.Data.IDataReader rd, Func f) - { - while(rd.Read()) - yield return f(); - } - - private System.Reflection.PropertyInfo[] GetORMFields() - { - var flags = - System.Reflection.BindingFlags.FlattenHierarchy | - System.Reflection.BindingFlags.Instance | - System.Reflection.BindingFlags.Public; - - var supportedPropertyTypes = new Type[] { - typeof(long), - typeof(string), - typeof(bool), - typeof(DateTime) - }; - - return - (from n in typeof(T).GetProperties(flags) - where supportedPropertyTypes.Contains(n.PropertyType) || n.PropertyType.IsEnum - select n).ToArray(); - } - - private IEnumerable ReadFromDb(string whereclause, params object[] args) - { - var properties = GetORMFields(); - - var sql = string.Format( - @"SELECT ""{0}"" FROM ""{1}"" {2} {3}", - string.Join(@""", """, properties.Select(x => x.Name)), - typeof(T).Name, - string.IsNullOrWhiteSpace(whereclause) ? "" : " WHERE ", - whereclause ?? "" - ); - - return ReadFromDb((rd) => { - var item = Activator.CreateInstance(); - for(var i = 0; i < properties.Length; i++) - { - var prop = properties[i]; - - if (prop.PropertyType.IsEnum) - prop.SetValue(item, ConvertToEnum(prop.PropertyType, rd, i, Enum.GetValues(prop.PropertyType).GetValue(0)), null); - else if (prop.PropertyType == typeof(string)) - prop.SetValue(item, ConvertToString(rd, i), null); - else if (prop.PropertyType == typeof(long)) - prop.SetValue(item, ConvertToInt64(rd, i), null); - else if (prop.PropertyType == typeof(bool)) - prop.SetValue(item, ConvertToBoolean(rd, i), null); - else if (prop.PropertyType == typeof(DateTime)) - prop.SetValue(item, ConvertToDateTime(rd, i), null); - } - - return item; - }, sql, args); - } - - private void OverwriteAndUpdateDb(System.Data.IDbTransaction transaction, string deleteSql, object[] deleteArgs, IEnumerable values, bool updateExisting) - { - var properties = GetORMFields(); - var idfield = properties.FirstOrDefault(x => x.Name == "ID"); - properties = properties.Where(x => x.Name != "ID").ToArray(); - - string sql; - - if (updateExisting) - { - sql = string.Format( - @"UPDATE ""{0}"" SET {1} WHERE ""ID""=?", - typeof(T).Name, - string.Join(@", ", properties.Select(x => string.Format(@"""{0}""=?", x.Name))) - ); - - properties = properties.Union(new System.Reflection.PropertyInfo[] { idfield }).ToArray(); - } - else - { - - sql = string.Format( - @"INSERT INTO ""{0}"" (""{1}"") VALUES ({2})", - typeof(T).Name, - string.Join(@""", """, properties.Select(x => x.Name)), - string.Join(@", ", properties.Select(x => "?")) - ); - } - - OverwriteAndUpdateDb(transaction, deleteSql, deleteArgs, values, sql, (item) => - { - return properties.Select((x) => - { - var val = x.GetValue(item, null); - - if (x.PropertyType.IsEnum) - val = val.ToString(); - else if (x.PropertyType == typeof(DateTime)) - val = Library.Utility.Utility.NormalizeDateTimeToEpochSeconds((DateTime)val); - - return val; - }).ToArray(); - }); - - if (!updateExisting && values.Count() == 1 && idfield != null) - using(var cmd = m_connection.CreateCommand()) - { - cmd.Transaction = transaction; - cmd.CommandText = @"SELECT last_insert_rowid();"; - if (idfield.PropertyType == typeof(string)) - idfield.SetValue(values.First(), ExecuteScalarString(cmd), null); - else - idfield.SetValue(values.First(), ExecuteScalarInt64(cmd), null); - } - } - - private IEnumerable ReadFromDb(Func f, string sql, params object[] args) - { - using(var cmd = m_connection.CreateCommand()) - { - cmd.CommandText = sql; - if (args != null) - foreach(var a in args) - { - var p = cmd.CreateParameter(); - p.Value = a; - cmd.Parameters.Add(p); - } - - return Read(cmd, f).ToArray(); - } - } - - private void OverwriteAndUpdateDb(System.Data.IDbTransaction transaction, string deleteSql, object[] deleteArgs, IEnumerable values, string insertSql, Func f) - { - using(var cmd = m_connection.CreateCommand()) - { - cmd.Transaction = transaction; - - if (!string.IsNullOrEmpty(deleteSql)) - { - cmd.CommandText = deleteSql; - if (deleteArgs != null) - foreach(var a in deleteArgs) - { - var p = cmd.CreateParameter(); - p.Value = a; - cmd.Parameters.Add(p); - } - - cmd.ExecuteNonQuery(); - cmd.Parameters.Clear(); - } - - cmd.CommandText = insertSql; - - foreach(var n in values) - { - var r = f(n); - if (r == null) - continue; - - while (cmd.Parameters.Count < r.Length) - cmd.Parameters.Add(cmd.CreateParameter()); - - for(var i = 0; i < r.Length; i++) - ((System.Data.IDbDataParameter)cmd.Parameters[i]).Value = r[i]; - - cmd.ExecuteNonQuery(); - } - } - } - - #region IDisposable implementation - public void Dispose() - { - if (m_errorcmd != null) - try { if (m_errorcmd != null) m_errorcmd.Dispose(); } - catch { } - finally { m_errorcmd = null; } - - - try - { - if (m_connection != null) - m_connection.Dispose(); - } - catch - { - } - } - #endregion - } - -} - +// Copyright (C) 2015, The Duplicati Team + +// http://www.duplicati.com, info@duplicati.com +// +// This library is free software; you can redistribute it and/or modify +// it under the terms of the GNU Lesser General Public License as +// published by the Free Software Foundation; either version 2.1 of the +// License, or (at your option) any later version. +// +// This library is distributed in the hope that it will be useful, but +// WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +// Lesser General Public License for more details. +// +// You should have received a copy of the GNU Lesser General Public +// License along with this library; if not, write to the Free Software +// Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA +using System; +using System.Collections.Generic; +using System.Linq; +using Duplicati.Server.Serialization.Interface; +using System.Text; +using Duplicati.Library.RestAPI; + +namespace Duplicati.Server.Database +{ + public class Connection : IDisposable + { + private readonly System.Data.IDbConnection m_connection; + private System.Data.IDbCommand m_errorcmd; + public readonly object m_lock = new object(); + public const int ANY_BACKUP_ID = -1; + public const int SERVER_SETTINGS_ID = -2; + private readonly Dictionary m_temporaryBackups = new Dictionary(); + + public Connection(System.Data.IDbConnection connection) + { + m_connection = connection; + m_errorcmd = m_connection.CreateCommand(); + m_errorcmd.CommandText = @"INSERT INTO ""ErrorLog"" (""BackupID"", ""Message"", ""Exception"", ""Timestamp"") VALUES (?,?,?,?)"; + for(var i = 0; i < 4; i++) + m_errorcmd.Parameters.Add(m_errorcmd.CreateParameter()); + + this.ApplicationSettings = new ServerSettings(this); + } + + public void LogError(string backupid, string message, Exception ex) + { + lock(m_lock) + { + if (!long.TryParse(backupid, out long id)) + id = -1; + ((System.Data.IDbDataParameter)m_errorcmd.Parameters[0]).Value = id; + ((System.Data.IDbDataParameter)m_errorcmd.Parameters[1]).Value = message; + ((System.Data.IDbDataParameter)m_errorcmd.Parameters[2]).Value = ex?.ToString(); + ((System.Data.IDbDataParameter)m_errorcmd.Parameters[3]).Value = Library.Utility.Utility.NormalizeDateTimeToEpochSeconds(DateTime.UtcNow); + m_errorcmd.ExecuteNonQuery(); + } + } + + internal void ExecuteWithCommand(Action f) + { + lock(m_lock) + using(var cmd = m_connection.CreateCommand()) + f(cmd); + } + + internal Serializable.ImportExportStructure PrepareBackupForExport(IBackup backup) + { + var scheduleId = GetScheduleIDsFromTags(new string[] { "ID=" + backup.ID }); + return new Serializable.ImportExportStructure() { + CreatedByVersion = System.Reflection.Assembly.GetExecutingAssembly().GetName().Version.ToString(), + Backup = (Database.Backup)backup, + Schedule = (Database.Schedule)(scheduleId.Any() ? GetSchedule(scheduleId.First()) : null), + DisplayNames = SpecialFolders.GetSourceNames(backup) + }; + } + + public string RegisterTemporaryBackup(IBackup backup) + { + lock(m_lock) + { + if (backup == null) + throw new ArgumentNullException(nameof(backup)); + if (backup.ID != null) + throw new ArgumentException("Backup is already active, cannot make temporary"); + + backup.ID = Guid.NewGuid().ToString("D"); + m_temporaryBackups.Add(backup.ID, (Backup)backup); + return backup.ID; + } + } + + public void UnregisterTemporaryBackup(IBackup backup) + { + lock(m_lock) + m_temporaryBackups.Remove(backup.ID); + } + + public void UpdateTemporaryBackup(IBackup backup) + { + lock(m_lock) + if (m_temporaryBackups.Remove(backup.ID)) + m_temporaryBackups.Add(backup.ID, (Backup)backup); + } + + public IBackup GetTemporaryBackup(string id) + { + if (string.IsNullOrEmpty(id)) + return null; + + lock(m_lock) + { + Backup b; + m_temporaryBackups.TryGetValue(id, out b); + return b; + } + } + + public ServerSettings ApplicationSettings { get; private set; } + + internal IDictionary GetMetadata(long id) + { + lock(m_lock) + return ReadFromDb( + (rd) => new KeyValuePair( + ConvertToString(rd, 0), + ConvertToString(rd, 1) + ), + @"SELECT ""Name"", ""Value"" FROM ""Metadata"" WHERE ""BackupID"" = ? ", id) + .ToDictionary((k) => k.Key, (k) => k.Value); + } + + internal void SetMetadata(IDictionary values, long id, System.Data.IDbTransaction transaction) + { + lock(m_lock) + using(var tr = transaction == null ? m_connection.BeginTransaction() : null) + { + OverwriteAndUpdateDb( + tr, + @"DELETE FROM ""Metadata"" WHERE ""BackupID"" = ?", new object[] { id }, + values ?? new Dictionary(), + @"INSERT INTO ""Metadata"" (""BackupID"", ""Name"", ""Value"") VALUES (?, ?, ?)", + (f) => new object[] { id, f.Key, f.Value } + ); + + if (tr != null) + tr.Commit(); + } + } + + internal IFilter[] GetFilters(long id) + { + lock(m_lock) + return ReadFromDb( + (rd) => (IFilter)new Filter() { + Order = ConvertToInt64(rd, 0), + Include = ConvertToBoolean(rd, 1), + Expression = ConvertToString(rd, 2) ?? "" + }, + @"SELECT ""Order"", ""Include"", ""Expression"" FROM ""Filter"" WHERE ""BackupID"" = ? ORDER BY ""Order"" ", id) + .ToArray(); + } + + internal void SetFilters(IEnumerable values, long id, System.Data.IDbTransaction transaction = null) + { + lock(m_lock) + using(var tr = transaction == null ? m_connection.BeginTransaction() : null) + { + OverwriteAndUpdateDb( + tr, + @"DELETE FROM ""Filter"" WHERE ""BackupID"" = ?", new object[] { id }, + values, + @"INSERT INTO ""Filter"" (""BackupID"", ""Order"", ""Include"", ""Expression"") VALUES (?, ?, ?, ?)", + (f) => new object[] { id, f.Order, f.Include, f.Expression } + ); + + if (tr != null) + tr.Commit(); + } + } + + public ISetting[] GetSettings(long id) + { + lock(m_lock) + return ReadFromDb( + (rd) => (ISetting)new Setting() { + Filter = ConvertToString(rd, 0) ?? "", + Name = ConvertToString(rd, 1) ?? "", + Value = ConvertToString(rd, 2) ?? "" + //TODO: Attach the argument information + }, + @"SELECT ""Filter"", ""Name"", ""Value"" FROM ""Option"" WHERE ""BackupID"" = ?", id) + .ToArray(); + } + + internal void SetSettings(IEnumerable values, long id, System.Data.IDbTransaction transaction = null) + { + lock(m_lock) + using(var tr = transaction == null ? m_connection.BeginTransaction() : null) + { + OverwriteAndUpdateDb( + tr, + @"DELETE FROM ""Option"" WHERE ""BackupID"" = ?", new object[] { id }, + values, + @"INSERT INTO ""Option"" (""BackupID"", ""Filter"", ""Name"", ""Value"") VALUES (?, ?, ?, ?)", + (f) => { + if (Duplicati.Server.WebServer.Server.PASSWORD_PLACEHOLDER.Equals(f.Value)) + throw new Exception("Attempted to save a property with the placeholder password"); + return new object[] { id, f.Filter ?? "", f.Name, f.Value ?? "" }; + } + ); + + if (tr != null) + tr.Commit(); + } + } + + internal string[] GetSources(long id) + { + lock(m_lock) + return ReadFromDb( + (rd) => ConvertToString(rd, 0), + @"SELECT ""Path"" FROM ""Source"" WHERE ""BackupID"" = ?", id) + .ToArray(); + } + + internal void SetSources(IEnumerable values, long id, System.Data.IDbTransaction transaction) + { + lock(m_lock) + using(var tr = transaction == null ? m_connection.BeginTransaction() : null) + { + OverwriteAndUpdateDb( + tr, + @"DELETE FROM ""Source"" WHERE ""BackupID"" = ?", new object[] { id }, + values, + @"INSERT INTO ""Source"" (""BackupID"", ""Path"") VALUES (?, ?)", + (f) => new object[] { id, f } + ); + + if (tr != null) + tr.Commit(); + } + } + + internal long[] GetBackupIDsForTags(string[] tags) + { + if (tags == null || tags.Length == 0) + return new long[0]; + + if (tags.Length == 1 && tags[0].StartsWith("ID=", StringComparison.Ordinal)) + return new long[] { long.Parse(tags[0].Substring("ID=".Length)) }; + + lock(m_lock) + using(var cmd = m_connection.CreateCommand()) + { + var sb = new StringBuilder(); + + foreach(var t in tags) + { + if (sb.Length != 0) + sb.Append(" OR "); + sb.Append(@" ("","" || ""Tags"" || "","" LIKE ""%,"" || ? || "",%"") "); + + var p = cmd.CreateParameter(); + p.Value = t; + cmd.Parameters.Add(p); + } + + cmd.CommandText = @"SELECT ""ID"" FROM ""Backup"" WHERE " + sb; + + return Read(cmd, (rd) => ConvertToInt64(rd, 0)).ToArray(); + } + } + + internal IBackup GetBackup(string id) + { + if (string.IsNullOrWhiteSpace(id)) + throw new ArgumentNullException(nameof(id)); + + return long.TryParse(id, out long lid) ? GetBackup(lid) : GetTemporaryBackup(id); + } + + internal IBackup GetBackup(long id) + { + lock(m_lock) + { + var bk = ReadFromDb( + (rd) => new Backup { + ID = ConvertToInt64(rd, 0).ToString(), + Name = ConvertToString(rd, 1), + Description = ConvertToString(rd, 2), + Tags = (ConvertToString(rd, 3) ?? "").Split(new char[] { ',' }, StringSplitOptions.RemoveEmptyEntries), + TargetURL = ConvertToString(rd, 4), + DBPath = ConvertToString(rd, 5), + }, + @"SELECT ""ID"", ""Name"", ""Description"", ""Tags"", ""TargetURL"", ""DBPath"" FROM ""Backup"" WHERE ID = ?", id) + .FirstOrDefault(); + + if (bk != null) + bk.LoadChildren(this); + + return bk; + } + } + + internal ISchedule GetSchedule(long id) + { + lock(m_lock) + { + var bk = ReadFromDb( + (rd) => new Schedule { + ID = ConvertToInt64(rd, 0), + Tags = (ConvertToString(rd, 1) ?? "").Split(new char[] { ',' }, StringSplitOptions.RemoveEmptyEntries), + Time = ConvertToDateTime(rd, 2), + Repeat = ConvertToString(rd, 3), + LastRun = ConvertToDateTime(rd, 4), + Rule = ConvertToString(rd, 5), + }, + @"SELECT ""ID"", ""Tags"", ""Time"", ""Repeat"", ""LastRun"", ""Rule"" FROM ""Schedule"" WHERE ID = ?", id) + .FirstOrDefault(); + + return bk; + } + } + + internal Boolean IsUnencryptedOrPassphraseStored(long id) + { + lock (m_lock) + { + var usesEncryption = ReadFromDb( + (rd) => ConvertToBoolean(rd, 0), + @"SELECT VALUE != """" FROM ""Option"" WHERE BackupID = ? AND NAME='encryption-module'", id) + .FirstOrDefault(); + + if (!usesEncryption) + { + return true; + } + + return ReadFromDb( + (rd) => ConvertToBoolean(rd, 0), + @"SELECT VALUE != """" FROM ""Option"" WHERE BackupID = ? AND NAME='passphrase'", id) + .FirstOrDefault(); + } + } + + internal long[] GetScheduleIDsFromTags(string[] tags) + { + if (tags == null || tags.Length == 0) + return new long[0]; + + lock(m_lock) + using(var cmd = m_connection.CreateCommand()) + { + var sb = new StringBuilder(); + + foreach(var t in tags) + { + if (sb.Length != 0) + sb.Append(" OR "); + sb.Append(@" ("","" || ""Tags"" || "","" LIKE ""%,"" || ? || "",%"") "); + + var p = cmd.CreateParameter(); + p.Value = t; + cmd.Parameters.Add(p); + } + + cmd.CommandText = @"SELECT ""ID"" FROM ""Schedule"" WHERE " + sb; + + return Read(cmd, (rd) => ConvertToInt64(rd, 0)).ToArray(); + } + } + + public void AddOrUpdateBackupAndSchedule(IBackup item, ISchedule schedule) + { + AddOrUpdateBackup(item, true, schedule); + } + + public string ValidateBackup(IBackup item, ISchedule schedule) + { + if (string.IsNullOrWhiteSpace(item.Name)) + return "Missing a name"; + + if (string.IsNullOrWhiteSpace(item.TargetURL)) + return "Missing a target"; + + if (item.Sources == null || item.Sources.Any(x => string.IsNullOrWhiteSpace(x)) || item.Sources.Length == 0) + return "Invalid source list"; + + var disabled_encryption = false; + var passphrase = string.Empty; + var gpgAsymmetricEncryption = false; + if (item.Settings != null) + { + foreach (var s in item.Settings) + + if (string.Equals(s.Name, "--no-encryption", StringComparison.OrdinalIgnoreCase)) + disabled_encryption = string.IsNullOrWhiteSpace(s.Value) || Library.Utility.Utility.ParseBool(s.Value, false); + else if (string.Equals(s.Name, "passphrase", StringComparison.OrdinalIgnoreCase)) + passphrase = s.Value; + else if (string.Equals(s.Name, "keep-versions", StringComparison.OrdinalIgnoreCase)) + { + int i; + if (!int.TryParse(s.Value, out i) || i <= 0) + return "Retention value must be a positive integer"; + } + else if (string.Equals(s.Name, "keep-time", StringComparison.OrdinalIgnoreCase)) + { + try + { + var ts = Library.Utility.Timeparser.ParseTimeSpan(s.Value); + if (ts <= TimeSpan.FromMinutes(5)) + return "Retention value must be more than 5 minutes"; + } + catch + { + return "Retention value must be a valid timespan"; + } + } + else if (string.Equals(s.Name, "dblock-size", StringComparison.OrdinalIgnoreCase)) + { + try + { + var ds = Library.Utility.Sizeparser.ParseSize(s.Value); + if (ds < 1024 * 1024) + return "DBlock size must be at least 1MB"; + } + catch + { + return "DBlock value must be a valid size string"; + } + } + else if (string.Equals(s.Name, "--blocksize", StringComparison.OrdinalIgnoreCase)) + { + try + { + var ds = Library.Utility.Sizeparser.ParseSize(s.Value); + if (ds < 1024 || ds > int.MaxValue) + return "The blocksize must be at least 1KB"; + } + catch + { + return "The blocksize value must be a valid size string"; + } + } + else if (string.Equals(s.Name, "--prefix", StringComparison.OrdinalIgnoreCase)) + { + if (!string.IsNullOrWhiteSpace(s.Value) && s.Value.Contains("-")) + return "The prefix cannot contain hyphens (-)"; + } + else if (string.Equals(s.Name, "--gpg-encryption-command", StringComparison.OrdinalIgnoreCase)) { + gpgAsymmetricEncryption = string.Equals(s.Value, "--encrypt", StringComparison.OrdinalIgnoreCase); + } + } + + if (!disabled_encryption && !gpgAsymmetricEncryption && string.IsNullOrWhiteSpace(passphrase)) + return "Missing passphrase"; + + if (schedule != null) + { + try + { + var ts = Library.Utility.Timeparser.ParseTimeSpan(schedule.Repeat); + if (ts <= TimeSpan.FromMinutes(5)) + return "Schedule repetition time must be more than 5 minutes"; + } + catch + { + return "Schedule repetition value must be a valid timespan"; + } + + } + + return null; + } + + internal void UpdateBackupDBPath(IBackup item, string path) + { + lock (m_lock) + { + using (var tr = m_connection.BeginTransaction()) + { + using (var cmd = m_connection.CreateCommand()) + { + cmd.Transaction = tr; + cmd.Parameters.Add(cmd.CreateParameter()); + ((System.Data.IDbDataParameter) cmd.Parameters[0]).Value = path; + cmd.Parameters.Add(cmd.CreateParameter()); + ((System.Data.IDbDataParameter) cmd.Parameters[1]).Value = item.ID; + + cmd.CommandText = @"UPDATE ""Backup"" SET ""DBPath""=? WHERE ""ID""=?"; + cmd.ExecuteNonQuery(); + tr.Commit(); + } + } + } + + FIXMEGlobal.IncrementLastDataUpdateID(); + FIXMEGlobal.StatusEventNotifyer.SignalNewEvent(); + } + + private void AddOrUpdateBackup(IBackup item, bool updateSchedule, ISchedule schedule) + { + lock(m_lock) + { + bool update = item.ID != null; + if (!update && item.DBPath == null) + { + var folder = FIXMEGlobal.DataFolder; + if (!System.IO.Directory.Exists(folder)) + System.IO.Directory.CreateDirectory(folder); + + for(var i = 0; i < 100; i++) + { + var guess = System.IO.Path.Combine(folder, System.IO.Path.ChangeExtension(Duplicati.Library.Main.DatabaseLocator.GenerateRandomName(), ".sqlite")); + if (!System.IO.File.Exists(guess)) + { + ((Backup)item).DBPath = guess; + break; + } + } + + if (item.DBPath == null) + throw new Exception("Unable to generate a unique database file name"); + } + + using(var tr = m_connection.BeginTransaction()) + { + OverwriteAndUpdateDb( + tr, + null, + new object[] { long.Parse(item.ID ?? "-1") }, + new IBackup[] { item }, + update ? + @"UPDATE ""Backup"" SET ""Name""=?, ""Description""=?, ""Tags""=?, ""TargetURL""=? WHERE ""ID""=?" : + @"INSERT INTO ""Backup"" (""Name"", ""Description"", ""Tags"", ""TargetURL"", ""DBPath"") VALUES (?,?,?,?,?)", + (n) => { + + if (n.TargetURL.IndexOf(Duplicati.Server.WebServer.Server.PASSWORD_PLACEHOLDER, StringComparison.Ordinal) >= 0) + throw new Exception("Attempted to save a backup with the password placeholder"); + if (update && long.Parse(n.ID) <= 0) + throw new Exception("Invalid update, cannot update application settings through update method"); + + return new object[] { + n.Name, + n.Description ?? "" , // Description is optional but the column is set to NOT NULL, an additional check is welcome + string.Join(",", n.Tags ?? new string[0]), + n.TargetURL, + update ? item.ID : n.DBPath + }; + }); + + if (!update) + using(var cmd = m_connection.CreateCommand()) + { + cmd.Transaction = tr; + cmd.CommandText = @"SELECT last_insert_rowid();"; + item.ID = ExecuteScalarInt64(cmd).ToString(); + } + + var id = long.Parse(item.ID); + + if (long.Parse(item.ID) <= 0) + throw new Exception("Invalid addition, cannot update application settings through update method"); + + SetSources(item.Sources, id, tr); + SetSettings(item.Settings, id, tr); + SetFilters(item.Filters, id, tr); + SetMetadata(item.Metadata, id, tr); + + if (updateSchedule) + { + var tags = new string[] { "ID=" + item.ID }; + var existing = GetScheduleIDsFromTags(tags); + if (schedule == null && existing.Any()) + DeleteFromDb("Schedule", existing.First(), tr); + else if (schedule != null) + { + if (existing.Any()) + { + var cur = GetSchedule(existing.First()); + cur.AllowedDays = schedule.AllowedDays; + cur.Repeat = schedule.Repeat; + cur.Tags = schedule.Tags; + cur.Time = schedule.Time; + + schedule = cur; + } + else + { + schedule.ID = -1; + } + + schedule.Tags = tags; + AddOrUpdateSchedule(schedule, tr); + } + } + + tr.Commit(); + FIXMEGlobal.IncrementLastDataUpdateID(); + FIXMEGlobal.StatusEventNotifyer.SignalNewEvent(); + } + } + } + + internal void AddOrUpdateSchedule(ISchedule item) + { + lock(m_lock) + using(var tr = m_connection.BeginTransaction()) + { + AddOrUpdateSchedule(item, tr); + tr.Commit(); + FIXMEGlobal.IncrementLastDataUpdateID(); + FIXMEGlobal.StatusEventNotifyer.SignalNewEvent(); + } + } + + private void AddOrUpdateSchedule(ISchedule item, System.Data.IDbTransaction tr) + { + lock(m_lock) + { + bool update = item.ID >= 0; + OverwriteAndUpdateDb( + tr, + null, + new object[] { item.ID }, + new ISchedule[] { item }, + update ? + @"UPDATE ""Schedule"" SET ""Tags""=?, ""Time""=?, ""Repeat""=?, ""LastRun""=?, ""Rule""=? WHERE ""ID""=?" : + @"INSERT INTO ""Schedule"" (""Tags"", ""Time"", ""Repeat"", ""LastRun"", ""Rule"") VALUES (?,?,?,?,?)", + (n) => new object[] { + string.Join(",", n.Tags), + Library.Utility.Utility.NormalizeDateTimeToEpochSeconds(n.Time), + n.Repeat, + Library.Utility.Utility.NormalizeDateTimeToEpochSeconds(n.LastRun), + n.Rule ?? "", + update ? (object)item.ID : null + }); + + if (!update) + using(var cmd = m_connection.CreateCommand()) + { + cmd.Transaction = tr; + cmd.CommandText = @"SELECT last_insert_rowid();"; + item.ID = ExecuteScalarInt64(cmd); + } + } + } + + public void DeleteBackup(long ID) + { + if (ID < 0) + return; + + lock(m_lock) + { + using(var tr = m_connection.BeginTransaction()) + { + var existing = GetScheduleIDsFromTags(new string[] { "ID=" + ID.ToString() }); + if (existing.Any()) + DeleteFromDb("Schedule", existing.First(), tr); + + DeleteFromDb("ErrorLog", ID, "BackupID", tr); + DeleteFromDb("Filter", ID, "BackupID", tr); + DeleteFromDb("Log", ID, "BackupID", tr); + DeleteFromDb("Metadata", ID, "BackupID", tr); + DeleteFromDb("Option", ID, "BackupID", tr); + DeleteFromDb("Source", ID, "BackupID", tr); + + DeleteFromDb("Backup", ID, tr); + + tr.Commit(); + } + } + + FIXMEGlobal.IncrementLastDataUpdateID(); + FIXMEGlobal.StatusEventNotifyer.SignalNewEvent(); + } + + public void DeleteBackup(IBackup backup) + { + if (backup.IsTemporary) + UnregisterTemporaryBackup(backup); + else + DeleteBackup(long.Parse(backup.ID)); + } + + public void DeleteSchedule(long ID) + { + if (ID < 0) + return; + + lock(m_lock) + DeleteFromDb("Schedule", ID); + + FIXMEGlobal.IncrementLastDataUpdateID(); + FIXMEGlobal.StatusEventNotifyer.SignalNewEvent(); + } + + public void DeleteSchedule(ISchedule schedule) + { + DeleteSchedule(schedule.ID); + } + + public IBackup[] Backups + { + get + { + lock(m_lock) + { + var lst = ReadFromDb( + (rd) => (IBackup)new Backup() { + ID = ConvertToInt64(rd, 0).ToString(), + Name = ConvertToString(rd, 1), + Description = ConvertToString(rd, 2), + Tags = (ConvertToString(rd, 3) ?? "").Split(new char[] { ',' }, StringSplitOptions.RemoveEmptyEntries), + TargetURL = ConvertToString(rd, 4), + DBPath = ConvertToString(rd, 5), + }, + @"SELECT ""ID"", ""Name"", ""Description"", ""Tags"", ""TargetURL"", ""DBPath"" FROM ""Backup"" ") + .ToArray(); + + foreach(var n in lst) + n.Metadata = GetMetadata(long.Parse(n.ID)); + + + return lst; + } + } + } + + public ISchedule[] Schedules + { + get + { + lock(m_lock) + return ReadFromDb( + (rd) => (ISchedule)new Schedule() { + ID = ConvertToInt64(rd, 0), + Tags = (ConvertToString(rd, 1) ?? "").Split(new char[] { ',' }, StringSplitOptions.RemoveEmptyEntries), + Time = ConvertToDateTime(rd, 2), + Repeat = ConvertToString(rd, 3), + LastRun = ConvertToDateTime(rd, 4), + Rule = ConvertToString(rd, 5), + }, + @"SELECT ""ID"", ""Tags"", ""Time"", ""Repeat"", ""LastRun"", ""Rule"" FROM ""Schedule"" ") + .ToArray(); + } + } + + + public IFilter[] Filters + { + get { return GetFilters(ANY_BACKUP_ID); } + set { SetFilters(value, ANY_BACKUP_ID); } + } + + public ISetting[] Settings + { + get { return GetSettings(ANY_BACKUP_ID); } + set { SetSettings(value, ANY_BACKUP_ID); } + } + + public INotification[] GetNotifications() + { + lock(m_lock) + return ReadFromDb(null).Cast().ToArray(); + } + + public bool DismissNotification(long id) + { + lock(m_lock) + { + var notifications = GetNotifications(); + var cur = notifications.FirstOrDefault(x => x.ID == id); + if (cur == null) + return false; + + DeleteFromDb(typeof(Notification).Name, id); + FIXMEGlobal.DataConnection.ApplicationSettings.UnackedError = notifications.Any(x => x.ID != id && x.Type == Duplicati.Server.Serialization.NotificationType.Error); + FIXMEGlobal.DataConnection.ApplicationSettings.UnackedWarning = notifications.Any(x => x.ID != id && x.Type == Duplicati.Server.Serialization.NotificationType.Warning); + } + + FIXMEGlobal.IncrementLastNotificationUpdateID(); + FIXMEGlobal.StatusEventNotifyer.SignalNewEvent(); + + return true; + } + + public void RegisterNotification(Serialization.NotificationType type, string title, string message, Exception ex, string backupid, string action, string logid, string messageid, string logtag, Func conflicthandler) + { + lock(m_lock) + { + var notification = new Notification() + { + ID = -1, + Type = type, + Title = title, + Message = message, + Exception = ex == null ? "" : ex.ToString(), + BackupID = backupid, + Action = action ?? "", + Timestamp = DateTime.UtcNow, + LogEntryID = logid, + MessageID = messageid, + MessageLogTag = logtag + }; + + var conflictResult = conflicthandler(notification, GetNotifications()); + if (conflictResult == null) + return; + + if (conflictResult != notification) + DeleteFromDb(typeof(Notification).Name, conflictResult.ID); + + OverwriteAndUpdateDb(null, null, null, new Notification[] { notification }, false); + + if (type == Duplicati.Server.Serialization.NotificationType.Error) + FIXMEGlobal.DataConnection.ApplicationSettings.UnackedError = true; + else if (type == Duplicati.Server.Serialization.NotificationType.Warning) + FIXMEGlobal.DataConnection.ApplicationSettings.UnackedWarning = true; + } + + FIXMEGlobal.IncrementLastNotificationUpdateID(); + FIXMEGlobal.StatusEventNotifyer.SignalNewEvent(); + } + + //Workaround to clean up the database after invalid settings update + public void FixInvalidBackupId() + { + using(var cmd = m_connection.CreateCommand()) + using (var tr = m_connection.BeginTransaction()) + { + cmd.Transaction = tr; + cmd.Parameters.Add(cmd.CreateParameter()); + ((System.Data.IDbDataParameter)cmd.Parameters[0]).Value = -1; + cmd.CommandText = @"DELETE FROM ""Option"" WHERE ""BackupID"" = ?"; + cmd.ExecuteNonQuery(); + cmd.CommandText = @"DELETE FROM ""Metadata"" WHERE ""BackupID"" = ?"; + cmd.ExecuteNonQuery(); + cmd.CommandText = @"DELETE FROM ""Filter"" WHERE ""BackupID"" = ?"; + cmd.ExecuteNonQuery(); + cmd.CommandText = @"DELETE FROM ""Source"" WHERE ""BackupID"" = ?"; + cmd.ExecuteNonQuery(); + + cmd.Parameters.Clear(); + cmd.Parameters.Add(cmd.CreateParameter()); + + ((System.Data.IDbDataParameter)cmd.Parameters[0]).Value = "ID=-1"; + cmd.CommandText = @"DELETE FROM ""Schedule"" WHERE ""Tags"" = ?"; + cmd.ExecuteNonQuery(); + tr.Commit(); + } + + ApplicationSettings.FixedInvalidBackupId = true; + } + + public string[] GetUISettingsSchemes() + { + lock(m_lock) + return ReadFromDb( + (rd) => ConvertToString(rd, 0) ?? "", + @"SELECT DISTINCT ""Scheme"" FROM ""UIStorage""") + .ToArray(); + } + + public IDictionary GetUISettings(string scheme) + { + lock(m_lock) + return ReadFromDb( + (rd) => new KeyValuePair( + ConvertToString(rd, 0) ?? "", + ConvertToString(rd, 1) ?? "" + ), + @"SELECT ""Key"", ""Value"" FROM ""UIStorage"" WHERE ""Scheme"" = ?", + scheme) + .GroupBy(x => x.Key) + .ToDictionary(x => x.Key, x => x.Last().Value); + } + + public void SetUISettings(string scheme, IDictionary values, System.Data.IDbTransaction transaction = null) + { + lock(m_lock) + using(var tr = transaction == null ? m_connection.BeginTransaction() : null) + { + OverwriteAndUpdateDb( + tr, + @"DELETE FROM ""UIStorage"" WHERE ""Scheme"" = ?", new object[] { scheme }, + values, + @"INSERT INTO ""UIStorage"" (""Scheme"", ""Key"", ""Value"") VALUES (?, ?, ?)", + (f) => { + return new object[] { scheme, f.Key ?? "", f.Value ?? "" }; + } + ); + + if (tr != null) + tr.Commit(); + } + } + + public void UpdateUISettings(string scheme, IDictionary values, System.Data.IDbTransaction transaction = null) + { + lock (m_lock) + using (var tr = transaction == null ? m_connection.BeginTransaction() : null) + { + OverwriteAndUpdateDb( + tr, + @"DELETE FROM ""UIStorage"" WHERE ""Scheme"" = ? AND ""Key"" IN (?)", new object[] { scheme, values.Keys }, + values.Where(x => x.Value != null), + @"INSERT INTO ""UIStorage"" (""Scheme"", ""Key"", ""Value"") VALUES (?, ?, ?)", + (f) => + { + return new object[] { scheme, f.Key ?? "", f.Value ?? "" }; + } + ); + + if (tr != null) + tr.Commit(); + } + } + + public TempFile[] GetTempFiles() + { + lock(m_lock) + return ReadFromDb(null).ToArray(); + } + + public void DeleteTempFile(long id) + { + lock(m_lock) + DeleteFromDb(typeof(TempFile).Name, id); + } + + public long RegisterTempFile(string origin, string path, DateTime expires) + { + var tempfile = new TempFile() { + Timestamp = DateTime.Now, + Origin = origin, + Path = path, + Expires = expires + }; + + OverwriteAndUpdateDb(null, null, null, new TempFile[] { tempfile }, false); + + return tempfile.ID; + } + + public void PurgeLogData(DateTime purgeDate) + { + var t = Library.Utility.Utility.NormalizeDateTimeToEpochSeconds(purgeDate); + + using(var tr = m_connection.BeginTransaction()) + using(var cmd = m_connection.CreateCommand()) + { + cmd.Transaction = tr; + cmd.CommandText = @"DELETE FROM ""ErrorLog"" WHERE ""Timestamp"" < ?"; + cmd.Parameters.Add(cmd.CreateParameter()); + ((System.Data.IDataParameter)cmd.Parameters[0]).Value = t; + cmd.ExecuteNonQuery(); + + tr.Commit(); + } + } + + private static DateTime ConvertToDateTime(System.Data.IDataReader rd, int index) + { + var unixTime = ConvertToInt64(rd, index); + return unixTime == 0 ? new DateTime(0) : Library.Utility.Utility.EPOCH.AddSeconds(unixTime); + } + + private static bool ConvertToBoolean(System.Data.IDataReader rd, int index) + { + return ConvertToInt64(rd, index) == 1; + } + + private static string ConvertToString(System.Data.IDataReader rd, int index) + { + var r = rd.GetValue(index); + return r == null || r == DBNull.Value ? null : r.ToString(); + } + + private static long ConvertToInt64(System.Data.IDataReader rd, int index) + { + try + { + if (!rd.IsDBNull(index)) + return rd.GetInt64(index); + } + catch + { + } + + return -1; + } + + private static long ExecuteScalarInt64(System.Data.IDbCommand cmd, long defaultValue = -1) + { + using(var rd = cmd.ExecuteReader()) + return rd.Read() ? ConvertToInt64(rd, 0) : defaultValue; + } + + private static string ExecuteScalarString(System.Data.IDbCommand cmd) + { + using(var rd = cmd.ExecuteReader()) + return rd.Read() ? ConvertToString(rd, 0) : null; + + } + + private object ConvertToEnum(Type enumType, System.Data.IDataReader rd, int index, object @default) + { + try + { + return Enum.Parse(enumType, ConvertToString(rd, index)); + } + catch + { + } + + return @default; + } + + // Overloaded function for legacy functionality + private bool DeleteFromDb(string tablename, long id, System.Data.IDbTransaction transaction = null) + { + return DeleteFromDb(tablename, id, "ID", transaction); + } + + // New function that allows to delete rows from tables with arbitrary identifier values (e.g. ID or BackupID) + private bool DeleteFromDb(string tablename, long id, string identifier, System.Data.IDbTransaction transaction = null) + { + if (transaction == null) + { + using(var tr = m_connection.BeginTransaction()) + { + var r = DeleteFromDb(tablename, id, tr); + tr.Commit(); + return r; + } + } + else + { + using(var cmd = m_connection.CreateCommand()) + { + cmd.Transaction = transaction; + cmd.CommandText = string.Format(@"DELETE FROM ""{0}"" WHERE ""{1}""=?", tablename, identifier); + var p = cmd.CreateParameter(); + p.Value = id; + cmd.Parameters.Add(p); + + var r = cmd.ExecuteNonQuery(); + // Roll back the transaction if more than 1 ID was deleted. Multiple "BackupID" rows being deleted isn't a problem. + if (identifier == "ID" && r > 1) + throw new Exception(string.Format("Too many records attempted deleted from table {0} for id {1}: {2}", tablename, id, r)); + return r == 1; + } + } + } + + private static IEnumerable Read(System.Data.IDbCommand cmd, Func f) + { + using(var rd = cmd.ExecuteReader()) + while(rd.Read()) + yield return f(rd); + } + + private static IEnumerable Read(System.Data.IDataReader rd, Func f) + { + while(rd.Read()) + yield return f(); + } + + private System.Reflection.PropertyInfo[] GetORMFields() + { + var flags = + System.Reflection.BindingFlags.FlattenHierarchy | + System.Reflection.BindingFlags.Instance | + System.Reflection.BindingFlags.Public; + + var supportedPropertyTypes = new Type[] { + typeof(long), + typeof(string), + typeof(bool), + typeof(DateTime) + }; + + return + (from n in typeof(T).GetProperties(flags) + where supportedPropertyTypes.Contains(n.PropertyType) || n.PropertyType.IsEnum + select n).ToArray(); + } + + private IEnumerable ReadFromDb(string whereclause, params object[] args) + { + var properties = GetORMFields(); + + var sql = string.Format( + @"SELECT ""{0}"" FROM ""{1}"" {2} {3}", + string.Join(@""", """, properties.Select(x => x.Name)), + typeof(T).Name, + string.IsNullOrWhiteSpace(whereclause) ? "" : " WHERE ", + whereclause ?? "" + ); + + return ReadFromDb((rd) => { + var item = Activator.CreateInstance(); + for(var i = 0; i < properties.Length; i++) + { + var prop = properties[i]; + + if (prop.PropertyType.IsEnum) + prop.SetValue(item, ConvertToEnum(prop.PropertyType, rd, i, Enum.GetValues(prop.PropertyType).GetValue(0)), null); + else if (prop.PropertyType == typeof(string)) + prop.SetValue(item, ConvertToString(rd, i), null); + else if (prop.PropertyType == typeof(long)) + prop.SetValue(item, ConvertToInt64(rd, i), null); + else if (prop.PropertyType == typeof(bool)) + prop.SetValue(item, ConvertToBoolean(rd, i), null); + else if (prop.PropertyType == typeof(DateTime)) + prop.SetValue(item, ConvertToDateTime(rd, i), null); + } + + return item; + }, sql, args); + } + + private void OverwriteAndUpdateDb(System.Data.IDbTransaction transaction, string deleteSql, object[] deleteArgs, IEnumerable values, bool updateExisting) + { + var properties = GetORMFields(); + var idfield = properties.FirstOrDefault(x => x.Name == "ID"); + properties = properties.Where(x => x.Name != "ID").ToArray(); + + string sql; + + if (updateExisting) + { + sql = string.Format( + @"UPDATE ""{0}"" SET {1} WHERE ""ID""=?", + typeof(T).Name, + string.Join(@", ", properties.Select(x => string.Format(@"""{0}""=?", x.Name))) + ); + + properties = properties.Union(new System.Reflection.PropertyInfo[] { idfield }).ToArray(); + } + else + { + + sql = string.Format( + @"INSERT INTO ""{0}"" (""{1}"") VALUES ({2})", + typeof(T).Name, + string.Join(@""", """, properties.Select(x => x.Name)), + string.Join(@", ", properties.Select(x => "?")) + ); + } + + OverwriteAndUpdateDb(transaction, deleteSql, deleteArgs, values, sql, (item) => + { + return properties.Select((x) => + { + var val = x.GetValue(item, null); + + if (x.PropertyType.IsEnum) + val = val.ToString(); + else if (x.PropertyType == typeof(DateTime)) + val = Library.Utility.Utility.NormalizeDateTimeToEpochSeconds((DateTime)val); + + return val; + }).ToArray(); + }); + + if (!updateExisting && values.Count() == 1 && idfield != null) + using(var cmd = m_connection.CreateCommand()) + { + cmd.Transaction = transaction; + cmd.CommandText = @"SELECT last_insert_rowid();"; + if (idfield.PropertyType == typeof(string)) + idfield.SetValue(values.First(), ExecuteScalarString(cmd), null); + else + idfield.SetValue(values.First(), ExecuteScalarInt64(cmd), null); + } + } + + private IEnumerable ReadFromDb(Func f, string sql, params object[] args) + { + using(var cmd = m_connection.CreateCommand()) + { + cmd.CommandText = sql; + if (args != null) + foreach(var a in args) + { + var p = cmd.CreateParameter(); + p.Value = a; + cmd.Parameters.Add(p); + } + + return Read(cmd, f).ToArray(); + } + } + + private void OverwriteAndUpdateDb(System.Data.IDbTransaction transaction, string deleteSql, object[] deleteArgs, IEnumerable values, string insertSql, Func f) + { + using(var cmd = m_connection.CreateCommand()) + { + cmd.Transaction = transaction; + + if (!string.IsNullOrEmpty(deleteSql)) + { + cmd.CommandText = deleteSql; + if (deleteArgs != null) + foreach(var a in deleteArgs) + { + var p = cmd.CreateParameter(); + p.Value = a; + cmd.Parameters.Add(p); + } + + cmd.ExecuteNonQuery(); + cmd.Parameters.Clear(); + } + + cmd.CommandText = insertSql; + + foreach(var n in values) + { + var r = f(n); + if (r == null) + continue; + + while (cmd.Parameters.Count < r.Length) + cmd.Parameters.Add(cmd.CreateParameter()); + + for(var i = 0; i < r.Length; i++) + ((System.Data.IDbDataParameter)cmd.Parameters[i]).Value = r[i]; + + cmd.ExecuteNonQuery(); + } + } + } + + #region IDisposable implementation + public void Dispose() + { + if (m_errorcmd != null) + try { if (m_errorcmd != null) m_errorcmd.Dispose(); } + catch { } + finally { m_errorcmd = null; } + + + try + { + if (m_connection != null) + m_connection.Dispose(); + } + catch + { + } + } + #endregion + } + +} + diff --git a/Duplicati/Server/Database/Database schema/1. Add Notifications.sql b/Duplicati.Library.RestAPI/Database/Database schema/1. Add Notifications.sql similarity index 95% rename from Duplicati/Server/Database/Database schema/1. Add Notifications.sql rename to Duplicati.Library.RestAPI/Database/Database schema/1. Add Notifications.sql index 578ef8ee8..9dc3c8b75 100644 --- a/Duplicati/Server/Database/Database schema/1. Add Notifications.sql +++ b/Duplicati.Library.RestAPI/Database/Database schema/1. Add Notifications.sql @@ -1,14 +1,14 @@ -/* -Notifications not yet acknowledged by the user -*/ -CREATE TABLE "Notification" ( - "ID" INTEGER PRIMARY KEY, - "Type" TEXT NOT NULL, - "Title" TEXT NOT NULL, - "Message" TEXT NOT NULL, - "Exception" TEXT NOT NULL, - "BackupID" TEXT NULL, - "Action" TEXT NOT NULL, - "Timestamp" INTEGER NOT NULL -); - +/* +Notifications not yet acknowledged by the user +*/ +CREATE TABLE "Notification" ( + "ID" INTEGER PRIMARY KEY, + "Type" TEXT NOT NULL, + "Title" TEXT NOT NULL, + "Message" TEXT NOT NULL, + "Exception" TEXT NOT NULL, + "BackupID" TEXT NULL, + "Action" TEXT NOT NULL, + "Timestamp" INTEGER NOT NULL +); + diff --git a/Duplicati/Server/Database/Database schema/2. Add UIStorage.sql b/Duplicati.Library.RestAPI/Database/Database schema/2. Add UIStorage.sql similarity index 94% rename from Duplicati/Server/Database/Database schema/2. Add UIStorage.sql rename to Duplicati.Library.RestAPI/Database/Database schema/2. Add UIStorage.sql index 2c33babc2..b0339749a 100644 --- a/Duplicati/Server/Database/Database schema/2. Add UIStorage.sql +++ b/Duplicati.Library.RestAPI/Database/Database schema/2. Add UIStorage.sql @@ -1,9 +1,9 @@ -/* -Key/value storage for frontends -*/ -CREATE TABLE "UIStorage" ( - "Scheme" TEXT NOT NULL, - "Key" TEXT NOT NULL, - "Value" TEXT NOT NULL -); - +/* +Key/value storage for frontends +*/ +CREATE TABLE "UIStorage" ( + "Scheme" TEXT NOT NULL, + "Key" TEXT NOT NULL, + "Value" TEXT NOT NULL +); + diff --git a/Duplicati/Server/Database/Database schema/3. Add temp file storage.sql b/Duplicati.Library.RestAPI/Database/Database schema/3. Add temp file storage.sql similarity index 95% rename from Duplicati/Server/Database/Database schema/3. Add temp file storage.sql rename to Duplicati.Library.RestAPI/Database/Database schema/3. Add temp file storage.sql index 1f77cd181..67d71370a 100644 --- a/Duplicati/Server/Database/Database schema/3. Add temp file storage.sql +++ b/Duplicati.Library.RestAPI/Database/Database schema/3. Add temp file storage.sql @@ -1,11 +1,11 @@ -/* -Long-term temporary file records -*/ -CREATE TABLE "TempFile" ( - "ID" INTEGER PRIMARY KEY, - "Origin" TEXT NOT NULL, - "Path" TEXT NOT NULL, - "Timestamp" INTEGER NOT NULL, - "Expires" INTEGER NOT NULL -); - +/* +Long-term temporary file records +*/ +CREATE TABLE "TempFile" ( + "ID" INTEGER PRIMARY KEY, + "Origin" TEXT NOT NULL, + "Path" TEXT NOT NULL, + "Timestamp" INTEGER NOT NULL, + "Expires" INTEGER NOT NULL +); + diff --git a/Duplicati/Server/Database/Database schema/4. Add autoincrement to backup id.sql b/Duplicati.Library.RestAPI/Database/Database schema/4. Add autoincrement to backup id.sql similarity index 100% rename from Duplicati/Server/Database/Database schema/4. Add autoincrement to backup id.sql rename to Duplicati.Library.RestAPI/Database/Database schema/4. Add autoincrement to backup id.sql diff --git a/Duplicati/Server/Database/Database schema/5. Extend notification table.sql b/Duplicati.Library.RestAPI/Database/Database schema/5. Extend notification table.sql similarity index 100% rename from Duplicati/Server/Database/Database schema/5. Extend notification table.sql rename to Duplicati.Library.RestAPI/Database/Database schema/5. Extend notification table.sql diff --git a/Duplicati/Server/Database/Database schema/6. Add Description to Backup.sql b/Duplicati.Library.RestAPI/Database/Database schema/6. Add Description to Backup.sql similarity index 100% rename from Duplicati/Server/Database/Database schema/6. Add Description to Backup.sql rename to Duplicati.Library.RestAPI/Database/Database schema/6. Add Description to Backup.sql diff --git a/Duplicati/Server/Database/Database schema/Schema.sql b/Duplicati.Library.RestAPI/Database/Database schema/Schema.sql similarity index 96% rename from Duplicati/Server/Database/Database schema/Schema.sql rename to Duplicati.Library.RestAPI/Database/Database schema/Schema.sql index 97c237ae2..b05876d22 100644 --- a/Duplicati/Server/Database/Database schema/Schema.sql +++ b/Duplicati.Library.RestAPI/Database/Database schema/Schema.sql @@ -1,158 +1,158 @@ -/* - * The primary table that stores all backups. - * - * The name and tag are free form user strings. - * The tags are comma separated - * the TargetURL is the url to remote storage, - * and the DBPath is the path to the local database - */ -CREATE TABLE "Backup" ( - "ID" INTEGER PRIMARY KEY AUTOINCREMENT, - "Name" TEXT NOT NULL, - "Description" TEXT NOT NULL DEFAULT '', - "Tags" TEXT NOT NULL, - "TargetURL" TEXT NOT NULL, - "DBPath" TEXT NOT NULL -); - -/* - * The table that stores all schedules - * - * Tags is a comma separated parsed field that indicates - * which backups to run when activated. - * special tags are ID:1 which means backup with ID = 1 - * - * Time is the scheduled time, and lastRun is the last time the backup was executed - * - * Rule is a special parsed field - */ -CREATE TABLE "Schedule" ( - "ID" INTEGER PRIMARY KEY, - "Tags" TEXT NOT NULL, - "Time" INTEGER NOT NULL, - "Repeat" TEXT NOT NULL, - "LastRun" INTEGER NOT NULL, - "Rule" TEXT NOT NULL -); - -/* - * The source table is a list of source folders and files - */ -CREATE TABLE "Source" ( - "BackupID" INTEGER NOT NULL, - "Path" TEXT NOT NULL -); - -/* - * The filter table contains all filters associated with a backup. - * The special backupID -1 means "applied to all backups" - * The expression is the filter, if the filter is a regular - * expression, it is surrounded by hard brackets [ ] - */ -CREATE TABLE "Filter" ( - "BackupID" INTEGER NOT NULL, - "Order" INTEGER NOT NULL, - "Include" INTEGER NOT NULL, - "Expression" TEXT NOT NULL -); - -/* - * All options are stored in this table - * - * The special backupID -1 means "applied to all backups". - * - * The filter is used to indicate what the option applies to, - * for instance backend:s3 will only apply to backends of type S3 - * - * The name and value are the option name and value - */ -CREATE TABLE "Option" ( - "BackupID" INTEGER NOT NULL, - "Filter" TEXT NOT NULL, - "Name" TEXT NOT NULL, - "Value" TEXT NOT NULL -); - -/* - * Recorded metadata about a backup - * This table contains metadata, such as when the backup was last started, - * how long it took, how many files there were, how big the backup set was, - * how much data was uploaded, downloaded, how fast, how much space is left, - * and similar data. Programs can use this information to improve the display, - * but cannot count on these values being present - */ -CREATE TABLE "Metadata" ( - "BackupID" INTEGER NOT NULL, - "Name" TEXT NOT NULL, - "Value" TEXT NOT NULL -); - -/* - * The log of operations initiated by the scheduler/user - */ -CREATE TABLE "Log" ( - "BackupID" INTEGER NOT NULL, - "Description" TEXT NOT NULL, - "Start" INTEGER NOT NULL, - "Finish" INTEGER NOT NULL, - "Result" TEXT NOT NULL, - "SuggestedIcon" TEXT NOT NULL -); - -/* - * The log of errors - */ -CREATE TABLE "ErrorLog" ( - "BackupID" INTEGER, - "Message" TEXT NOT NULL, - "Exception" TEXT, - "Timestamp" INTEGER NOT NULL -); - -/* -Internal version tracking -*/ -CREATE TABLE "Version" ( - "ID" INTEGER PRIMARY KEY, - "Version" INTEGER NOT NULL -); - -/* -Notifications not yet acknowledged by the user -*/ -CREATE TABLE "Notification" ( - "ID" INTEGER PRIMARY KEY, - "Type" TEXT NOT NULL, - "Title" TEXT NOT NULL, - "Message" TEXT NOT NULL, - "Exception" TEXT NOT NULL, - "BackupID" TEXT NULL, - "Action" TEXT NOT NULL, - "Timestamp" INTEGER NOT NULL, - "LogEntryID" TEXT NULL, - "MessageID" TEXT NULL, - "MessageLogTag" TEXT NULL -); - -/* -Key/value storage for frontends -*/ -CREATE TABLE "UIStorage" ( - "Scheme" TEXT NOT NULL, - "Key" TEXT NOT NULL, - "Value" TEXT NOT NULL -); - -/* -Long-term temporary file records -*/ -CREATE TABLE "TempFile" ( - "ID" INTEGER PRIMARY KEY, - "Origin" TEXT NOT NULL, - "Path" TEXT NOT NULL, - "Timestamp" INTEGER NOT NULL, - "Expires" INTEGER NOT NULL -); - -INSERT INTO "Version" ("Version") VALUES (6); - +/* + * The primary table that stores all backups. + * + * The name and tag are free form user strings. + * The tags are comma separated + * the TargetURL is the url to remote storage, + * and the DBPath is the path to the local database + */ +CREATE TABLE "Backup" ( + "ID" INTEGER PRIMARY KEY AUTOINCREMENT, + "Name" TEXT NOT NULL, + "Description" TEXT NOT NULL DEFAULT '', + "Tags" TEXT NOT NULL, + "TargetURL" TEXT NOT NULL, + "DBPath" TEXT NOT NULL +); + +/* + * The table that stores all schedules + * + * Tags is a comma separated parsed field that indicates + * which backups to run when activated. + * special tags are ID:1 which means backup with ID = 1 + * + * Time is the scheduled time, and lastRun is the last time the backup was executed + * + * Rule is a special parsed field + */ +CREATE TABLE "Schedule" ( + "ID" INTEGER PRIMARY KEY, + "Tags" TEXT NOT NULL, + "Time" INTEGER NOT NULL, + "Repeat" TEXT NOT NULL, + "LastRun" INTEGER NOT NULL, + "Rule" TEXT NOT NULL +); + +/* + * The source table is a list of source folders and files + */ +CREATE TABLE "Source" ( + "BackupID" INTEGER NOT NULL, + "Path" TEXT NOT NULL +); + +/* + * The filter table contains all filters associated with a backup. + * The special backupID -1 means "applied to all backups" + * The expression is the filter, if the filter is a regular + * expression, it is surrounded by hard brackets [ ] + */ +CREATE TABLE "Filter" ( + "BackupID" INTEGER NOT NULL, + "Order" INTEGER NOT NULL, + "Include" INTEGER NOT NULL, + "Expression" TEXT NOT NULL +); + +/* + * All options are stored in this table + * + * The special backupID -1 means "applied to all backups". + * + * The filter is used to indicate what the option applies to, + * for instance backend:s3 will only apply to backends of type S3 + * + * The name and value are the option name and value + */ +CREATE TABLE "Option" ( + "BackupID" INTEGER NOT NULL, + "Filter" TEXT NOT NULL, + "Name" TEXT NOT NULL, + "Value" TEXT NOT NULL +); + +/* + * Recorded metadata about a backup + * This table contains metadata, such as when the backup was last started, + * how long it took, how many files there were, how big the backup set was, + * how much data was uploaded, downloaded, how fast, how much space is left, + * and similar data. Programs can use this information to improve the display, + * but cannot count on these values being present + */ +CREATE TABLE "Metadata" ( + "BackupID" INTEGER NOT NULL, + "Name" TEXT NOT NULL, + "Value" TEXT NOT NULL +); + +/* + * The log of operations initiated by the scheduler/user + */ +CREATE TABLE "Log" ( + "BackupID" INTEGER NOT NULL, + "Description" TEXT NOT NULL, + "Start" INTEGER NOT NULL, + "Finish" INTEGER NOT NULL, + "Result" TEXT NOT NULL, + "SuggestedIcon" TEXT NOT NULL +); + +/* + * The log of errors + */ +CREATE TABLE "ErrorLog" ( + "BackupID" INTEGER, + "Message" TEXT NOT NULL, + "Exception" TEXT, + "Timestamp" INTEGER NOT NULL +); + +/* +Internal version tracking +*/ +CREATE TABLE "Version" ( + "ID" INTEGER PRIMARY KEY, + "Version" INTEGER NOT NULL +); + +/* +Notifications not yet acknowledged by the user +*/ +CREATE TABLE "Notification" ( + "ID" INTEGER PRIMARY KEY, + "Type" TEXT NOT NULL, + "Title" TEXT NOT NULL, + "Message" TEXT NOT NULL, + "Exception" TEXT NOT NULL, + "BackupID" TEXT NULL, + "Action" TEXT NOT NULL, + "Timestamp" INTEGER NOT NULL, + "LogEntryID" TEXT NULL, + "MessageID" TEXT NULL, + "MessageLogTag" TEXT NULL +); + +/* +Key/value storage for frontends +*/ +CREATE TABLE "UIStorage" ( + "Scheme" TEXT NOT NULL, + "Key" TEXT NOT NULL, + "Value" TEXT NOT NULL +); + +/* +Long-term temporary file records +*/ +CREATE TABLE "TempFile" ( + "ID" INTEGER PRIMARY KEY, + "Origin" TEXT NOT NULL, + "Path" TEXT NOT NULL, + "Timestamp" INTEGER NOT NULL, + "Expires" INTEGER NOT NULL +); + +INSERT INTO "Version" ("Version") VALUES (6); + diff --git a/Duplicati.Library.RestAPI/Database/DatabaseConnectionSchemaMarker.cs b/Duplicati.Library.RestAPI/Database/DatabaseConnectionSchemaMarker.cs new file mode 100644 index 000000000..110f8477f --- /dev/null +++ b/Duplicati.Library.RestAPI/Database/DatabaseConnectionSchemaMarker.cs @@ -0,0 +1,12 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Duplicati.Library.RestAPI.Database +{ + public static class DatabaseConnectionSchemaMarker + { + } +} diff --git a/Duplicati/Server/Database/Filter.cs b/Duplicati.Library.RestAPI/Database/Filter.cs similarity index 97% rename from Duplicati/Server/Database/Filter.cs rename to Duplicati.Library.RestAPI/Database/Filter.cs index 7f14e2bda..60733d83c 100644 --- a/Duplicati/Server/Database/Filter.cs +++ b/Duplicati.Library.RestAPI/Database/Filter.cs @@ -1,31 +1,31 @@ -// Copyright (C) 2015, The Duplicati Team - -// http://www.duplicati.com, info@duplicati.com -// -// This library is free software; you can redistribute it and/or modify -// it under the terms of the GNU Lesser General Public License as -// published by the Free Software Foundation; either version 2.1 of the -// License, or (at your option) any later version. -// -// This library is distributed in the hope that it will be useful, but -// WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU -// Lesser General Public License for more details. -// -// You should have received a copy of the GNU Lesser General Public -// License along with this library; if not, write to the Free Software -// Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA -using System; - -namespace Duplicati.Server.Database -{ - public class Filter : Duplicati.Server.Serialization.Interface.IFilter - { - public long Order { get; set; } - - public bool Include { get; set; } - - public string Expression { get; set; } - } -} - +// Copyright (C) 2015, The Duplicati Team + +// http://www.duplicati.com, info@duplicati.com +// +// This library is free software; you can redistribute it and/or modify +// it under the terms of the GNU Lesser General Public License as +// published by the Free Software Foundation; either version 2.1 of the +// License, or (at your option) any later version. +// +// This library is distributed in the hope that it will be useful, but +// WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +// Lesser General Public License for more details. +// +// You should have received a copy of the GNU Lesser General Public +// License along with this library; if not, write to the Free Software +// Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA +using System; + +namespace Duplicati.Server.Database +{ + public class Filter : Duplicati.Server.Serialization.Interface.IFilter + { + public long Order { get; set; } + + public bool Include { get; set; } + + public string Expression { get; set; } + } +} + diff --git a/Duplicati/Server/Database/Notification.cs b/Duplicati.Library.RestAPI/Database/Notification.cs similarity index 97% rename from Duplicati/Server/Database/Notification.cs rename to Duplicati.Library.RestAPI/Database/Notification.cs index 22b507701..328951d94 100644 --- a/Duplicati/Server/Database/Notification.cs +++ b/Duplicati.Library.RestAPI/Database/Notification.cs @@ -1,39 +1,39 @@ -// Copyright (C) 2015, The Duplicati Team - -// http://www.duplicati.com, info@duplicati.com -// -// This library is free software; you can redistribute it and/or modify -// it under the terms of the GNU Lesser General Public License as -// published by the Free Software Foundation; either version 2.1 of the -// License, or (at your option) any later version. -// -// This library is distributed in the hope that it will be useful, but -// WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU -// Lesser General Public License for more details. -// -// You should have received a copy of the GNU Lesser General Public -// License along with this library; if not, write to the Free Software -// Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA -using System; - -namespace Duplicati.Server.Database -{ - public class Notification : Server.Serialization.Interface.INotification - { - #region INotification implementation - public long ID { get; set; } - public Duplicati.Server.Serialization.NotificationType Type { get; set; } - public string Title { get; set; } - public string Message { get; set; } - public string Exception { get; set; } - public string BackupID { get; set; } - public string Action { get; set; } - public DateTime Timestamp { get; set; } - public string LogEntryID { get; set; } - public string MessageID { get; set; } - public string MessageLogTag { get; set; } - #endregion - } -} - +// Copyright (C) 2015, The Duplicati Team + +// http://www.duplicati.com, info@duplicati.com +// +// This library is free software; you can redistribute it and/or modify +// it under the terms of the GNU Lesser General Public License as +// published by the Free Software Foundation; either version 2.1 of the +// License, or (at your option) any later version. +// +// This library is distributed in the hope that it will be useful, but +// WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +// Lesser General Public License for more details. +// +// You should have received a copy of the GNU Lesser General Public +// License along with this library; if not, write to the Free Software +// Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA +using System; + +namespace Duplicati.Server.Database +{ + public class Notification : Server.Serialization.Interface.INotification + { + #region INotification implementation + public long ID { get; set; } + public Duplicati.Server.Serialization.NotificationType Type { get; set; } + public string Title { get; set; } + public string Message { get; set; } + public string Exception { get; set; } + public string BackupID { get; set; } + public string Action { get; set; } + public DateTime Timestamp { get; set; } + public string LogEntryID { get; set; } + public string MessageID { get; set; } + public string MessageLogTag { get; set; } + #endregion + } +} + diff --git a/Duplicati/Server/Database/Schedule.cs b/Duplicati.Library.RestAPI/Database/Schedule.cs similarity index 97% rename from Duplicati/Server/Database/Schedule.cs rename to Duplicati.Library.RestAPI/Database/Schedule.cs index 7df817d40..4f38a5978 100644 --- a/Duplicati/Server/Database/Schedule.cs +++ b/Duplicati.Library.RestAPI/Database/Schedule.cs @@ -1,79 +1,79 @@ -// Copyright (C) 2015, The Duplicati Team - -// http://www.duplicati.com, info@duplicati.com -// -// This library is free software; you can redistribute it and/or modify -// it under the terms of the GNU Lesser General Public License as -// published by the Free Software Foundation; either version 2.1 of the -// License, or (at your option) any later version. -// -// This library is distributed in the hope that it will be useful, but -// WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU -// Lesser General Public License for more details. -// -// You should have received a copy of the GNU Lesser General Public -// License along with this library; if not, write to the Free Software -// Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA -using System; -using System.Linq; -using Duplicati.Server.Serialization.Interface; - -namespace Duplicati.Server.Database -{ - public class Schedule : ISchedule - { - public long ID { get; set; } - public string[] Tags { get; set; } - public DateTime Time { get; set; } - public string Repeat { get; set; } - public DateTime LastRun { get; set; } - public string Rule { get; set; } - - public DayOfWeek[] AllowedDays - { - get - { - if (string.IsNullOrEmpty(this.Rule)) - return null; - - var days = (from n in this.Rule.Split(new string[] { ";" }, StringSplitOptions.RemoveEmptyEntries) - where n.StartsWith("AllowedWeekDays=", StringComparison.OrdinalIgnoreCase) - select n.Substring("AllowedWeekDays=".Length).Split(new char[] {','}, StringSplitOptions.RemoveEmptyEntries)) - .FirstOrDefault(); - - - if (days == null) - return null; - - return (from n in days - where Enum.TryParse(n, true, out _) - select (DayOfWeek)Enum.Parse(typeof(DayOfWeek), n, true)) - .ToArray(); - } - set - { - - var parts = - string.IsNullOrEmpty(this.Rule) ? - new string[0] : - (from n in this.Rule.Split(new string[] { ";" }, StringSplitOptions.RemoveEmptyEntries) - where !n.StartsWith("AllowedWeekDays=", StringComparison.OrdinalIgnoreCase) - select n); - - if (value != null && value.Length != 0) - parts = parts.Union(new string[] { - "AllowedWeekDays=" + - string.Join( - ",", - (from n in value - select Enum.GetName(typeof(DayOfWeek), n)).Distinct() - ) - }).Distinct(); - - this.Rule = string.Join(";", parts); - } - } - } -} - +// Copyright (C) 2015, The Duplicati Team + +// http://www.duplicati.com, info@duplicati.com +// +// This library is free software; you can redistribute it and/or modify +// it under the terms of the GNU Lesser General Public License as +// published by the Free Software Foundation; either version 2.1 of the +// License, or (at your option) any later version. +// +// This library is distributed in the hope that it will be useful, but +// WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +// Lesser General Public License for more details. +// +// You should have received a copy of the GNU Lesser General Public +// License along with this library; if not, write to the Free Software +// Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA +using System; +using System.Linq; +using Duplicati.Server.Serialization.Interface; + +namespace Duplicati.Server.Database +{ + public class Schedule : ISchedule + { + public long ID { get; set; } + public string[] Tags { get; set; } + public DateTime Time { get; set; } + public string Repeat { get; set; } + public DateTime LastRun { get; set; } + public string Rule { get; set; } + + public DayOfWeek[] AllowedDays + { + get + { + if (string.IsNullOrEmpty(this.Rule)) + return null; + + var days = (from n in this.Rule.Split(new string[] { ";" }, StringSplitOptions.RemoveEmptyEntries) + where n.StartsWith("AllowedWeekDays=", StringComparison.OrdinalIgnoreCase) + select n.Substring("AllowedWeekDays=".Length).Split(new char[] {','}, StringSplitOptions.RemoveEmptyEntries)) + .FirstOrDefault(); + + + if (days == null) + return null; + + return (from n in days + where Enum.TryParse(n, true, out _) + select (DayOfWeek)Enum.Parse(typeof(DayOfWeek), n, true)) + .ToArray(); + } + set + { + + var parts = + string.IsNullOrEmpty(this.Rule) ? + new string[0] : + (from n in this.Rule.Split(new string[] { ";" }, StringSplitOptions.RemoveEmptyEntries) + where !n.StartsWith("AllowedWeekDays=", StringComparison.OrdinalIgnoreCase) + select n); + + if (value != null && value.Length != 0) + parts = parts.Union(new string[] { + "AllowedWeekDays=" + + string.Join( + ",", + (from n in value + select Enum.GetName(typeof(DayOfWeek), n)).Distinct() + ) + }).Distinct(); + + this.Rule = string.Join(";", parts); + } + } + } +} + diff --git a/Duplicati/Server/Database/ServerSettings.cs b/Duplicati.Library.RestAPI/Database/ServerSettings.cs similarity index 95% rename from Duplicati/Server/Database/ServerSettings.cs rename to Duplicati.Library.RestAPI/Database/ServerSettings.cs index f39379867..92b02bcfa 100644 --- a/Duplicati/Server/Database/ServerSettings.cs +++ b/Duplicati.Library.RestAPI/Database/ServerSettings.cs @@ -1,208 +1,209 @@ -// Copyright (C) 2015, The Duplicati Team - -// http://www.duplicati.com, info@duplicati.com -// -// This library is free software; you can redistribute it and/or modify -// it under the terms of the GNU Lesser General Public License as -// published by the Free Software Foundation; either version 2.1 of the -// License, or (at your option) any later version. -// -// This library is distributed in the hope that it will be useful, but -// WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU -// Lesser General Public License for more details. -// -// You should have received a copy of the GNU Lesser General Public -// License along with this library; if not, write to the Free Software -// Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA -using System; -using System.Linq; -using System.Collections.Generic; -using System.Security.Cryptography; -using System.Security.Cryptography.X509Certificates; -using Duplicati.Library.Common; +// Copyright (C) 2015, The Duplicati Team -namespace Duplicati.Server.Database -{ - public class ServerSettings - { - private static class CONST - { - public const string STARTUP_DELAY = "startup-delay"; - public const string DOWNLOAD_SPEED_LIMIT = "max-download-speed"; - public const string UPLOAD_SPEED_LIMIT = "max-upload-speed"; - public const string THREAD_PRIORITY = "thread-priority"; - public const string LAST_WEBSERVER_PORT = "last-webserver-port"; - public const string IS_FIRST_RUN = "is-first-run"; - public const string SERVER_PORT_CHANGED = "server-port-changed"; - public const string SERVER_PASSPHRASE = "server-passphrase"; - public const string SERVER_PASSPHRASE_SALT = "server-passphrase-salt"; - public const string SERVER_PASSPHRASETRAYICON = "server-passphrase-trayicon"; - public const string SERVER_PASSPHRASETRAYICONHASH = "server-passphrase-trayicon-hash"; - public const string UPDATE_CHECK_LAST = "last-update-check"; - public const string UPDATE_CHECK_INTERVAL = "update-check-interval"; - public const string UPDATE_CHECK_NEW_VERSION = "update-check-latest"; - public const string UNACKED_ERROR = "unacked-error"; - public const string UNACKED_WARNING = "unacked-warning"; - public const string SERVER_LISTEN_INTERFACE = "server-listen-interface"; - public const string SERVER_SSL_CERTIFICATE = "server-ssl-certificate"; - public const string HAS_FIXED_INVALID_BACKUPID = "has-fixed-invalid-backup-id"; - public const string UPDATE_CHANNEL = "update-channel"; +// http://www.duplicati.com, info@duplicati.com +// +// This library is free software; you can redistribute it and/or modify +// it under the terms of the GNU Lesser General Public License as +// published by the Free Software Foundation; either version 2.1 of the +// License, or (at your option) any later version. +// +// This library is distributed in the hope that it will be useful, but +// WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +// Lesser General Public License for more details. +// +// You should have received a copy of the GNU Lesser General Public +// License along with this library; if not, write to the Free Software +// Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA +using System; +using System.Linq; +using System.Collections.Generic; +using System.Security.Cryptography; +using System.Security.Cryptography.X509Certificates; +using Duplicati.Library.Common; +using Duplicati.Library.RestAPI; + +namespace Duplicati.Server.Database +{ + public class ServerSettings + { + private static class CONST + { + public const string STARTUP_DELAY = "startup-delay"; + public const string DOWNLOAD_SPEED_LIMIT = "max-download-speed"; + public const string UPLOAD_SPEED_LIMIT = "max-upload-speed"; + public const string THREAD_PRIORITY = "thread-priority"; + public const string LAST_WEBSERVER_PORT = "last-webserver-port"; + public const string IS_FIRST_RUN = "is-first-run"; + public const string SERVER_PORT_CHANGED = "server-port-changed"; + public const string SERVER_PASSPHRASE = "server-passphrase"; + public const string SERVER_PASSPHRASE_SALT = "server-passphrase-salt"; + public const string SERVER_PASSPHRASETRAYICON = "server-passphrase-trayicon"; + public const string SERVER_PASSPHRASETRAYICONHASH = "server-passphrase-trayicon-hash"; + public const string UPDATE_CHECK_LAST = "last-update-check"; + public const string UPDATE_CHECK_INTERVAL = "update-check-interval"; + public const string UPDATE_CHECK_NEW_VERSION = "update-check-latest"; + public const string UNACKED_ERROR = "unacked-error"; + public const string UNACKED_WARNING = "unacked-warning"; + public const string SERVER_LISTEN_INTERFACE = "server-listen-interface"; + public const string SERVER_SSL_CERTIFICATE = "server-ssl-certificate"; + public const string HAS_FIXED_INVALID_BACKUPID = "has-fixed-invalid-backup-id"; + public const string UPDATE_CHANNEL = "update-channel"; public const string USAGE_REPORTER_LEVEL = "usage-reporter-level"; public const string HAS_ASKED_FOR_PASSWORD_PROTECTION = "has-asked-for-password-protection"; - public const string DISABLE_TRAY_ICON_LOGIN = "disable-tray-icon-login"; - public const string SERVER_ALLOWED_HOSTNAMES = "allowed-hostnames"; - } - - private readonly Dictionary settings; - private readonly Connection databaseConnection; - private Library.AutoUpdater.UpdateInfo m_latestUpdate; - - internal ServerSettings(Connection con) - { - settings = new Dictionary(); - databaseConnection = con; - ReloadSettings(); - } - - public void ReloadSettings() - { - lock(databaseConnection.m_lock) - { - settings.Clear(); - foreach(var n in typeof(CONST).GetFields(System.Reflection.BindingFlags.Public | System.Reflection.BindingFlags.DeclaredOnly | System.Reflection.BindingFlags.Static).Select(x => (string)x.GetValue(null))) - settings[n] = null; - foreach(var n in databaseConnection.GetSettings(Connection.SERVER_SETTINGS_ID)) - settings[n.Name] = n.Value; - } - } - - public void UpdateSettings(Dictionary newsettings, bool clearExisting) - { - if (newsettings == null) - throw new ArgumentNullException(); - - lock(databaseConnection.m_lock) - { - m_latestUpdate = null; - if (clearExisting) - settings.Clear(); - - foreach(var k in newsettings) - if (!clearExisting && newsettings[k.Key] == null && k.Key.StartsWith("--", StringComparison.Ordinal)) - settings.Remove(k.Key); - else - settings[k.Key] = newsettings[k.Key]; - - } - - SaveSettings(); - - if (newsettings.Keys.Contains(CONST.SERVER_PASSPHRASE)) - GenerateWebserverPasswordTrayIcon(); - } - - private void SaveSettings() - { - databaseConnection.SetSettings( - from n in settings - select (Duplicati.Server.Serialization.Interface.ISetting)new Setting() { - Filter = "", - Name = n.Key, - Value = n.Value - }, Database.Connection.SERVER_SETTINGS_ID); - - System.Threading.Interlocked.Increment(ref Program.LastDataUpdateID); - Program.StatusEventNotifyer.SignalNewEvent(); - - // In case the usage reporter is enabled or disabled, refresh now - Program.StartOrStopUsageReporter(); - // If throttle options were changed, update now - Program.UpdateThrottleSpeeds(); - } - - public string StartupDelayDuration - { - get - { - return settings[CONST.STARTUP_DELAY]; - } - set - { - lock(databaseConnection.m_lock) - settings[CONST.STARTUP_DELAY] = value; - SaveSettings(); - } - } - - public System.Threading.ThreadPriority? ThreadPriorityOverride - { - get - { - var tp = settings[CONST.THREAD_PRIORITY]; - if (string.IsNullOrEmpty(tp)) - return null; - - System.Threading.ThreadPriority r; - if (Enum.TryParse(tp, true, out r)) - return r; - - return null; - } - set - { - lock(databaseConnection.m_lock) - settings[CONST.THREAD_PRIORITY] = value.HasValue ? Enum.GetName(typeof(System.Threading.ThreadPriority), value.Value) : null; - } - } - - public string DownloadSpeedLimit - { - get - { - return settings[CONST.DOWNLOAD_SPEED_LIMIT]; - } - set - { - lock(databaseConnection.m_lock) - settings[CONST.DOWNLOAD_SPEED_LIMIT] = value; - SaveSettings(); - } - } - - public string UploadSpeedLimit - { - get - { - return settings[CONST.UPLOAD_SPEED_LIMIT]; - } - set - { - lock(databaseConnection.m_lock) - settings[CONST.UPLOAD_SPEED_LIMIT] = value; - SaveSettings(); - } - } - - public bool IsFirstRun - { - get - { - return Duplicati.Library.Utility.Utility.ParseBoolOption(settings, CONST.IS_FIRST_RUN); - } - set - { - lock(databaseConnection.m_lock) - settings[CONST.IS_FIRST_RUN] = value.ToString(); - SaveSettings(); - } + public const string DISABLE_TRAY_ICON_LOGIN = "disable-tray-icon-login"; + public const string SERVER_ALLOWED_HOSTNAMES = "allowed-hostnames"; + } + + private readonly Dictionary settings; + private readonly Connection databaseConnection; + private Library.AutoUpdater.UpdateInfo m_latestUpdate; + + internal ServerSettings(Connection con) + { + settings = new Dictionary(); + databaseConnection = con; + ReloadSettings(); + } + + public void ReloadSettings() + { + lock(databaseConnection.m_lock) + { + settings.Clear(); + foreach(var n in typeof(CONST).GetFields(System.Reflection.BindingFlags.Public | System.Reflection.BindingFlags.DeclaredOnly | System.Reflection.BindingFlags.Static).Select(x => (string)x.GetValue(null))) + settings[n] = null; + foreach(var n in databaseConnection.GetSettings(Connection.SERVER_SETTINGS_ID)) + settings[n.Name] = n.Value; + } + } + + public void UpdateSettings(Dictionary newsettings, bool clearExisting) + { + if (newsettings == null) + throw new ArgumentNullException(); + + lock(databaseConnection.m_lock) + { + m_latestUpdate = null; + if (clearExisting) + settings.Clear(); + + foreach(var k in newsettings) + if (!clearExisting && newsettings[k.Key] == null && k.Key.StartsWith("--", StringComparison.Ordinal)) + settings.Remove(k.Key); + else + settings[k.Key] = newsettings[k.Key]; + + } + + SaveSettings(); + + if (newsettings.Keys.Contains(CONST.SERVER_PASSPHRASE)) + GenerateWebserverPasswordTrayIcon(); + } + + private void SaveSettings() + { + databaseConnection.SetSettings( + from n in settings + select (Duplicati.Server.Serialization.Interface.ISetting)new Setting() { + Filter = "", + Name = n.Key, + Value = n.Value + }, Database.Connection.SERVER_SETTINGS_ID); + + FIXMEGlobal.IncrementLastDataUpdateID(); + FIXMEGlobal.StatusEventNotifyer.SignalNewEvent(); + + // In case the usage reporter is enabled or disabled, refresh now + FIXMEGlobal.StartOrStopUsageReporter(); + // If throttle options were changed, update now + FIXMEGlobal.UpdateThrottleSpeeds(); + } + + public string StartupDelayDuration + { + get + { + return settings[CONST.STARTUP_DELAY]; + } + set + { + lock(databaseConnection.m_lock) + settings[CONST.STARTUP_DELAY] = value; + SaveSettings(); + } + } + + public System.Threading.ThreadPriority? ThreadPriorityOverride + { + get + { + var tp = settings[CONST.THREAD_PRIORITY]; + if (string.IsNullOrEmpty(tp)) + return null; + + System.Threading.ThreadPriority r; + if (Enum.TryParse(tp, true, out r)) + return r; + + return null; + } + set + { + lock(databaseConnection.m_lock) + settings[CONST.THREAD_PRIORITY] = value.HasValue ? Enum.GetName(typeof(System.Threading.ThreadPriority), value.Value) : null; + } + } + + public string DownloadSpeedLimit + { + get + { + return settings[CONST.DOWNLOAD_SPEED_LIMIT]; + } + set + { + lock(databaseConnection.m_lock) + settings[CONST.DOWNLOAD_SPEED_LIMIT] = value; + SaveSettings(); + } + } + + public string UploadSpeedLimit + { + get + { + return settings[CONST.UPLOAD_SPEED_LIMIT]; + } + set + { + lock(databaseConnection.m_lock) + settings[CONST.UPLOAD_SPEED_LIMIT] = value; + SaveSettings(); + } + } + + public bool IsFirstRun + { + get + { + return Duplicati.Library.Utility.Utility.ParseBoolOption(settings, CONST.IS_FIRST_RUN); + } + set + { + lock(databaseConnection.m_lock) + settings[CONST.IS_FIRST_RUN] = value.ToString(); + SaveSettings(); + } } public bool HasAskedForPasswordProtection { get - { - return Duplicati.Library.Utility.Utility.ParseBoolOption(settings, CONST.HAS_ASKED_FOR_PASSWORD_PROTECTION); + { + return Duplicati.Library.Utility.Utility.ParseBoolOption(settings, CONST.HAS_ASKED_FOR_PASSWORD_PROTECTION); } set { @@ -212,352 +213,352 @@ namespace Duplicati.Server.Database } } - public bool UnackedError - { - get - { - return Duplicati.Library.Utility.Utility.ParseBool(settings[CONST.UNACKED_ERROR], false); - } - set - { - lock(databaseConnection.m_lock) - settings[CONST.UNACKED_ERROR] = value.ToString(); - SaveSettings(); - } - } - - public bool UnackedWarning - { - get - { - return Duplicati.Library.Utility.Utility.ParseBool(settings[CONST.UNACKED_WARNING], false); - } - set - { - lock(databaseConnection.m_lock) - settings[CONST.UNACKED_WARNING] = value.ToString(); - SaveSettings(); - } - } - - public bool ServerPortChanged - { - get - { - return Duplicati.Library.Utility.Utility.ParseBool(settings[CONST.SERVER_PORT_CHANGED], false); - } - set - { - lock(databaseConnection.m_lock) - settings[CONST.SERVER_PORT_CHANGED] = value.ToString(); - SaveSettings(); - } - } - - public bool DisableTrayIconLogin - { - get - { - return Duplicati.Library.Utility.Utility.ParseBool(settings[CONST.DISABLE_TRAY_ICON_LOGIN], false); - } - set - { - lock (databaseConnection.m_lock) - settings[CONST.DISABLE_TRAY_ICON_LOGIN] = value.ToString(); - SaveSettings(); - } - } - - public int LastWebserverPort - { - get - { - var tp = settings[CONST.LAST_WEBSERVER_PORT]; - int p; - if (string.IsNullOrEmpty(tp) || !int.TryParse(tp, out p)) - return -1; - - return p; - } - set - { - lock(databaseConnection.m_lock) - settings[CONST.LAST_WEBSERVER_PORT] = value.ToString(); - SaveSettings(); - } - } - - public string WebserverPassword - { - get - { - return settings[CONST.SERVER_PASSPHRASE]; - } - } - - public string WebserverPasswordSalt - { - get - { - return settings[CONST.SERVER_PASSPHRASE_SALT]; - } - } - - public void SetWebserverPassword(string password) - { - if (string.IsNullOrWhiteSpace(password)) - { - lock(databaseConnection.m_lock) - { - settings[CONST.SERVER_PASSPHRASE] = ""; - settings[CONST.SERVER_PASSPHRASE_SALT] = ""; - } - } - else - { - var prng = RandomNumberGenerator.Create(); - var buf = new byte[32]; - prng.GetBytes(buf); - var salt = Convert.ToBase64String(buf); - - var sha256 = System.Security.Cryptography.SHA256.Create(); - var str = System.Text.Encoding.UTF8.GetBytes(password); - - sha256.TransformBlock(str, 0, str.Length, str, 0); - sha256.TransformFinalBlock(buf, 0, buf.Length); - var pwd = Convert.ToBase64String(sha256.Hash); - - lock(databaseConnection.m_lock) - { - settings[CONST.SERVER_PASSPHRASE] = pwd; - settings[CONST.SERVER_PASSPHRASE_SALT] = salt; - } - } - - SaveSettings(); - } - - public void SetAllowedHostnames(string allowedHostnames) - { - lock (databaseConnection.m_lock) - settings[CONST.SERVER_ALLOWED_HOSTNAMES] = allowedHostnames; - - SaveSettings(); - } - - public string WebserverPasswordTrayIcon => settings[CONST.SERVER_PASSPHRASETRAYICON]; - - public string WebserverPasswordTrayIconHash => settings[CONST.SERVER_PASSPHRASETRAYICONHASH]; - - public string AllowedHostnames => settings[CONST.SERVER_ALLOWED_HOSTNAMES]; - - public void GenerateWebserverPasswordTrayIcon() - { - var password = ""; - var pwd = ""; - - if (!string.IsNullOrEmpty(settings[CONST.SERVER_PASSPHRASE])) - { - password = Guid.NewGuid().ToString(); - var buf = Convert.FromBase64String(settings[CONST.SERVER_PASSPHRASE_SALT]); - - var sha256 = System.Security.Cryptography.SHA256.Create(); - var str = System.Text.Encoding.UTF8.GetBytes(password); - - sha256.TransformBlock(str, 0, str.Length, str, 0); - sha256.TransformFinalBlock(buf, 0, buf.Length); - pwd = Convert.ToBase64String(sha256.Hash); - } - - lock (databaseConnection.m_lock) - { - settings[CONST.SERVER_PASSPHRASETRAYICON] = password; - settings[CONST.SERVER_PASSPHRASETRAYICONHASH] = pwd; - } - - SaveSettings(); - } - - public DateTime LastUpdateCheck - { - get - { - long t; - if (long.TryParse(settings[CONST.UPDATE_CHECK_LAST], out t)) - return new DateTime(t, DateTimeKind.Utc); - else - return new DateTime(0, DateTimeKind.Utc); - } - set - { - lock(databaseConnection.m_lock) - settings[CONST.UPDATE_CHECK_LAST] = value.ToUniversalTime().Ticks.ToString(); - SaveSettings(); - } - } - - public string UpdateCheckInterval - { - get - { - var tp = settings[CONST.UPDATE_CHECK_INTERVAL]; - if (string.IsNullOrWhiteSpace(tp)) - tp = "1W"; - - return tp; - } - set - { - lock(databaseConnection.m_lock) - settings[CONST.UPDATE_CHECK_INTERVAL] = value; - SaveSettings(); - Program.UpdatePoller.Reschedule(); - } - } - - public DateTime NextUpdateCheck - { - get - { - try - { - return Duplicati.Library.Utility.Timeparser.ParseTimeInterval(UpdateCheckInterval, LastUpdateCheck); - } - catch - { - return LastUpdateCheck.AddDays(7); - } - } - } - - public Library.AutoUpdater.UpdateInfo UpdatedVersion - { - get - { - if (string.IsNullOrWhiteSpace(settings[CONST.UPDATE_CHECK_NEW_VERSION])) - return null; - - try - { - if (m_latestUpdate != null) - return m_latestUpdate; - - using(var tr = new System.IO.StringReader(settings[CONST.UPDATE_CHECK_NEW_VERSION])) - return m_latestUpdate = Server.Serialization.Serializer.Deserialize(tr); - } - catch - { - } - - return null; - } - set - { - string result = null; - if (value != null) - { - var sb = new System.Text.StringBuilder(); - using(var tw = new System.IO.StringWriter(sb)) - Server.Serialization.Serializer.SerializeJson(tw, value); - - result = sb.ToString(); - } - - m_latestUpdate = value; - lock(databaseConnection.m_lock) - settings[CONST.UPDATE_CHECK_NEW_VERSION] = result; - - SaveSettings(); - } - } - - public string ServerListenInterface - { - get - { - return settings[CONST.SERVER_LISTEN_INTERFACE]; - } - set - { - lock(databaseConnection.m_lock) - settings[CONST.SERVER_LISTEN_INTERFACE] = value; - SaveSettings(); - } - } - - public X509Certificate2 ServerSSLCertificate - { - get - { - if (String.IsNullOrEmpty(settings[CONST.SERVER_SSL_CERTIFICATE])) - return null; - - if (Platform.IsClientWindows) - return new X509Certificate2(Convert.FromBase64String(settings[CONST.SERVER_SSL_CERTIFICATE])); - else - return new X509Certificate2(Convert.FromBase64String(settings[CONST.SERVER_SSL_CERTIFICATE]), ""); - } - set - { - if (value == null) - { - lock (databaseConnection.m_lock) - settings[CONST.SERVER_SSL_CERTIFICATE] = String.Empty; - } - else - { - if (Platform.IsClientWindows) - lock (databaseConnection.m_lock) - settings[CONST.SERVER_SSL_CERTIFICATE] = Convert.ToBase64String(value.Export(X509ContentType.Pkcs12)); - else - lock (databaseConnection.m_lock) - settings[CONST.SERVER_SSL_CERTIFICATE] = Convert.ToBase64String(value.Export(X509ContentType.Pkcs12, "")); - } - SaveSettings(); - } - } - - public bool FixedInvalidBackupId - { - get - { - return Duplicati.Library.Utility.Utility.ParseBool(settings[CONST.HAS_FIXED_INVALID_BACKUPID], false); - } - set - { - lock(databaseConnection.m_lock) - settings[CONST.HAS_FIXED_INVALID_BACKUPID] = value.ToString(); - SaveSettings(); - } - } - - public string UpdateChannel - { - get - { - return settings[CONST.UPDATE_CHANNEL]; - } - set - { - lock(databaseConnection.m_lock) - settings[CONST.UPDATE_CHANNEL] = value; - SaveSettings(); - } - } - - public string UsageReporterLevel - { - get - { - return settings[CONST.USAGE_REPORTER_LEVEL]; - } - set - { - lock(databaseConnection.m_lock) - settings[CONST.USAGE_REPORTER_LEVEL] = value; - SaveSettings(); - } - } - } -} - + public bool UnackedError + { + get + { + return Duplicati.Library.Utility.Utility.ParseBool(settings[CONST.UNACKED_ERROR], false); + } + set + { + lock(databaseConnection.m_lock) + settings[CONST.UNACKED_ERROR] = value.ToString(); + SaveSettings(); + } + } + + public bool UnackedWarning + { + get + { + return Duplicati.Library.Utility.Utility.ParseBool(settings[CONST.UNACKED_WARNING], false); + } + set + { + lock(databaseConnection.m_lock) + settings[CONST.UNACKED_WARNING] = value.ToString(); + SaveSettings(); + } + } + + public bool ServerPortChanged + { + get + { + return Duplicati.Library.Utility.Utility.ParseBool(settings[CONST.SERVER_PORT_CHANGED], false); + } + set + { + lock(databaseConnection.m_lock) + settings[CONST.SERVER_PORT_CHANGED] = value.ToString(); + SaveSettings(); + } + } + + public bool DisableTrayIconLogin + { + get + { + return Duplicati.Library.Utility.Utility.ParseBool(settings[CONST.DISABLE_TRAY_ICON_LOGIN], false); + } + set + { + lock (databaseConnection.m_lock) + settings[CONST.DISABLE_TRAY_ICON_LOGIN] = value.ToString(); + SaveSettings(); + } + } + + public int LastWebserverPort + { + get + { + var tp = settings[CONST.LAST_WEBSERVER_PORT]; + int p; + if (string.IsNullOrEmpty(tp) || !int.TryParse(tp, out p)) + return -1; + + return p; + } + set + { + lock(databaseConnection.m_lock) + settings[CONST.LAST_WEBSERVER_PORT] = value.ToString(); + SaveSettings(); + } + } + + public string WebserverPassword + { + get + { + return settings[CONST.SERVER_PASSPHRASE]; + } + } + + public string WebserverPasswordSalt + { + get + { + return settings[CONST.SERVER_PASSPHRASE_SALT]; + } + } + + public void SetWebserverPassword(string password) + { + if (string.IsNullOrWhiteSpace(password)) + { + lock(databaseConnection.m_lock) + { + settings[CONST.SERVER_PASSPHRASE] = ""; + settings[CONST.SERVER_PASSPHRASE_SALT] = ""; + } + } + else + { + var prng = RandomNumberGenerator.Create(); + var buf = new byte[32]; + prng.GetBytes(buf); + var salt = Convert.ToBase64String(buf); + + var sha256 = System.Security.Cryptography.SHA256.Create(); + var str = System.Text.Encoding.UTF8.GetBytes(password); + + sha256.TransformBlock(str, 0, str.Length, str, 0); + sha256.TransformFinalBlock(buf, 0, buf.Length); + var pwd = Convert.ToBase64String(sha256.Hash); + + lock(databaseConnection.m_lock) + { + settings[CONST.SERVER_PASSPHRASE] = pwd; + settings[CONST.SERVER_PASSPHRASE_SALT] = salt; + } + } + + SaveSettings(); + } + + public void SetAllowedHostnames(string allowedHostnames) + { + lock (databaseConnection.m_lock) + settings[CONST.SERVER_ALLOWED_HOSTNAMES] = allowedHostnames; + + SaveSettings(); + } + + public string WebserverPasswordTrayIcon => settings[CONST.SERVER_PASSPHRASETRAYICON]; + + public string WebserverPasswordTrayIconHash => settings[CONST.SERVER_PASSPHRASETRAYICONHASH]; + + public string AllowedHostnames => settings[CONST.SERVER_ALLOWED_HOSTNAMES]; + + public void GenerateWebserverPasswordTrayIcon() + { + var password = ""; + var pwd = ""; + + if (!string.IsNullOrEmpty(settings[CONST.SERVER_PASSPHRASE])) + { + password = Guid.NewGuid().ToString(); + var buf = Convert.FromBase64String(settings[CONST.SERVER_PASSPHRASE_SALT]); + + var sha256 = System.Security.Cryptography.SHA256.Create(); + var str = System.Text.Encoding.UTF8.GetBytes(password); + + sha256.TransformBlock(str, 0, str.Length, str, 0); + sha256.TransformFinalBlock(buf, 0, buf.Length); + pwd = Convert.ToBase64String(sha256.Hash); + } + + lock (databaseConnection.m_lock) + { + settings[CONST.SERVER_PASSPHRASETRAYICON] = password; + settings[CONST.SERVER_PASSPHRASETRAYICONHASH] = pwd; + } + + SaveSettings(); + } + + public DateTime LastUpdateCheck + { + get + { + long t; + if (long.TryParse(settings[CONST.UPDATE_CHECK_LAST], out t)) + return new DateTime(t, DateTimeKind.Utc); + else + return new DateTime(0, DateTimeKind.Utc); + } + set + { + lock(databaseConnection.m_lock) + settings[CONST.UPDATE_CHECK_LAST] = value.ToUniversalTime().Ticks.ToString(); + SaveSettings(); + } + } + + public string UpdateCheckInterval + { + get + { + var tp = settings[CONST.UPDATE_CHECK_INTERVAL]; + if (string.IsNullOrWhiteSpace(tp)) + tp = "1W"; + + return tp; + } + set + { + lock(databaseConnection.m_lock) + settings[CONST.UPDATE_CHECK_INTERVAL] = value; + SaveSettings(); + FIXMEGlobal.UpdatePoller.Reschedule(); + } + } + + public DateTime NextUpdateCheck + { + get + { + try + { + return Duplicati.Library.Utility.Timeparser.ParseTimeInterval(UpdateCheckInterval, LastUpdateCheck); + } + catch + { + return LastUpdateCheck.AddDays(7); + } + } + } + + public Library.AutoUpdater.UpdateInfo UpdatedVersion + { + get + { + if (string.IsNullOrWhiteSpace(settings[CONST.UPDATE_CHECK_NEW_VERSION])) + return null; + + try + { + if (m_latestUpdate != null) + return m_latestUpdate; + + using(var tr = new System.IO.StringReader(settings[CONST.UPDATE_CHECK_NEW_VERSION])) + return m_latestUpdate = Server.Serialization.Serializer.Deserialize(tr); + } + catch + { + } + + return null; + } + set + { + string result = null; + if (value != null) + { + var sb = new System.Text.StringBuilder(); + using(var tw = new System.IO.StringWriter(sb)) + Server.Serialization.Serializer.SerializeJson(tw, value); + + result = sb.ToString(); + } + + m_latestUpdate = value; + lock(databaseConnection.m_lock) + settings[CONST.UPDATE_CHECK_NEW_VERSION] = result; + + SaveSettings(); + } + } + + public string ServerListenInterface + { + get + { + return settings[CONST.SERVER_LISTEN_INTERFACE]; + } + set + { + lock(databaseConnection.m_lock) + settings[CONST.SERVER_LISTEN_INTERFACE] = value; + SaveSettings(); + } + } + + public X509Certificate2 ServerSSLCertificate + { + get + { + if (String.IsNullOrEmpty(settings[CONST.SERVER_SSL_CERTIFICATE])) + return null; + + if (Platform.IsClientWindows) + return new X509Certificate2(Convert.FromBase64String(settings[CONST.SERVER_SSL_CERTIFICATE])); + else + return new X509Certificate2(Convert.FromBase64String(settings[CONST.SERVER_SSL_CERTIFICATE]), ""); + } + set + { + if (value == null) + { + lock (databaseConnection.m_lock) + settings[CONST.SERVER_SSL_CERTIFICATE] = String.Empty; + } + else + { + if (Platform.IsClientWindows) + lock (databaseConnection.m_lock) + settings[CONST.SERVER_SSL_CERTIFICATE] = Convert.ToBase64String(value.Export(X509ContentType.Pkcs12)); + else + lock (databaseConnection.m_lock) + settings[CONST.SERVER_SSL_CERTIFICATE] = Convert.ToBase64String(value.Export(X509ContentType.Pkcs12, "")); + } + SaveSettings(); + } + } + + public bool FixedInvalidBackupId + { + get + { + return Duplicati.Library.Utility.Utility.ParseBool(settings[CONST.HAS_FIXED_INVALID_BACKUPID], false); + } + set + { + lock(databaseConnection.m_lock) + settings[CONST.HAS_FIXED_INVALID_BACKUPID] = value.ToString(); + SaveSettings(); + } + } + + public string UpdateChannel + { + get + { + return settings[CONST.UPDATE_CHANNEL]; + } + set + { + lock(databaseConnection.m_lock) + settings[CONST.UPDATE_CHANNEL] = value; + SaveSettings(); + } + } + + public string UsageReporterLevel + { + get + { + return settings[CONST.USAGE_REPORTER_LEVEL]; + } + set + { + lock(databaseConnection.m_lock) + settings[CONST.USAGE_REPORTER_LEVEL] = value; + SaveSettings(); + } + } + } +} + diff --git a/Duplicati/Server/Database/Setting.cs b/Duplicati.Library.RestAPI/Database/Setting.cs similarity index 97% rename from Duplicati/Server/Database/Setting.cs rename to Duplicati.Library.RestAPI/Database/Setting.cs index 84150bf1a..0fe096389 100644 --- a/Duplicati/Server/Database/Setting.cs +++ b/Duplicati.Library.RestAPI/Database/Setting.cs @@ -1,26 +1,26 @@ -// Copyright (C) 2015, The Duplicati Team - -// http://www.duplicati.com, info@duplicati.com -// -// This library is free software; you can redistribute it and/or modify -// it under the terms of the GNU Lesser General Public License as -// published by the Free Software Foundation; either version 2.1 of the -// License, or (at your option) any later version. -// -// This library is distributed in the hope that it will be useful, but -// WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU -// Lesser General Public License for more details. -// -// You should have received a copy of the GNU Lesser General Public -// License along with this library; if not, write to the Free Software -// Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA -using System; - -namespace Duplicati.Server.Database -{ - public class Setting : Duplicati.Server.Serialization.Implementations.Setting - { - } -} - +// Copyright (C) 2015, The Duplicati Team + +// http://www.duplicati.com, info@duplicati.com +// +// This library is free software; you can redistribute it and/or modify +// it under the terms of the GNU Lesser General Public License as +// published by the Free Software Foundation; either version 2.1 of the +// License, or (at your option) any later version. +// +// This library is distributed in the hope that it will be useful, but +// WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +// Lesser General Public License for more details. +// +// You should have received a copy of the GNU Lesser General Public +// License along with this library; if not, write to the Free Software +// Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA +using System; + +namespace Duplicati.Server.Database +{ + public class Setting : Duplicati.Server.Serialization.Implementations.Setting + { + } +} + diff --git a/Duplicati/Server/Database/TempFile.cs b/Duplicati.Library.RestAPI/Database/TempFile.cs similarity index 97% rename from Duplicati/Server/Database/TempFile.cs rename to Duplicati.Library.RestAPI/Database/TempFile.cs index b652a2920..ecbf2cfe8 100644 --- a/Duplicati/Server/Database/TempFile.cs +++ b/Duplicati.Library.RestAPI/Database/TempFile.cs @@ -1,31 +1,31 @@ -// Copyright (C) 2015, The Duplicati Team - -// http://www.duplicati.com, info@duplicati.com -// -// This library is free software; you can redistribute it and/or modify -// it under the terms of the GNU Lesser General Public License as -// published by the Free Software Foundation; either version 2.1 of the -// License, or (at your option) any later version. -// -// This library is distributed in the hope that it will be useful, but -// WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU -// Lesser General Public License for more details. -// -// You should have received a copy of the GNU Lesser General Public -// License along with this library; if not, write to the Free Software -// Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA -using System; - -namespace Duplicati.Server.Database -{ - public class TempFile - { - public long ID { get; set; } - public string Origin { get; set; } - public string Path { get; set; } - public DateTime Timestamp { get; set; } - public DateTime Expires { get; set; } - } -} - +// Copyright (C) 2015, The Duplicati Team + +// http://www.duplicati.com, info@duplicati.com +// +// This library is free software; you can redistribute it and/or modify +// it under the terms of the GNU Lesser General Public License as +// published by the Free Software Foundation; either version 2.1 of the +// License, or (at your option) any later version. +// +// This library is distributed in the hope that it will be useful, but +// WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +// Lesser General Public License for more details. +// +// You should have received a copy of the GNU Lesser General Public +// License along with this library; if not, write to the Free Software +// Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA +using System; + +namespace Duplicati.Server.Database +{ + public class TempFile + { + public long ID { get; set; } + public string Origin { get; set; } + public string Path { get; set; } + public DateTime Timestamp { get; set; } + public DateTime Expires { get; set; } + } +} + diff --git a/Duplicati.Library.RestAPI/Duplicati.Library.RestAPI.csproj b/Duplicati.Library.RestAPI/Duplicati.Library.RestAPI.csproj new file mode 100644 index 000000000..f350e3e34 --- /dev/null +++ b/Duplicati.Library.RestAPI/Duplicati.Library.RestAPI.csproj @@ -0,0 +1,35 @@ + + + + net6.0 + + + + + + + + + + + + + + + + + ..\thirdparty\HttpServer\HttpServer.dll + + + + + + + + + + + + + + diff --git a/Duplicati/Server/EventPollNotify.cs b/Duplicati.Library.RestAPI/EventPollNotify.cs similarity index 97% rename from Duplicati/Server/EventPollNotify.cs rename to Duplicati.Library.RestAPI/EventPollNotify.cs index 015e05af5..777dc4a1e 100644 --- a/Duplicati/Server/EventPollNotify.cs +++ b/Duplicati.Library.RestAPI/EventPollNotify.cs @@ -1,77 +1,77 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; - -namespace Duplicati.Server -{ - /// - /// This class handles synchronized waiting for events - /// - public class EventPollNotify - { - /// - /// The lock that grants exclusive access to control structures - /// - private readonly object m_lock = new object(); - /// - /// The current eventID - /// - private long m_eventNo = 0; - /// - /// The list of subscribed waiting threads - /// - private readonly Queue m_waitQueue = new Queue(); - - /// - /// An eventhandler for subscribing to event updates without blocking - /// - public event EventHandler NewEvent; - - /// - /// Gets the current event ID - /// - public long EventNo { get { return m_eventNo; } } - - /// - /// Call to wait for an event that is newer than the current known event - /// - /// The last known event id - /// The number of milliseconds to block - /// The current event id - public long Wait(long eventId, int milliseconds) - { - System.Threading.ManualResetEvent mre; - lock (m_lock) - { - //If a newer event has already occured, return immediately - if (eventId != m_eventNo) - return m_eventNo; - - //Otherwise register this thread as waiting - mre = new System.Threading.ManualResetEvent(false); - m_waitQueue.Enqueue(mre); - } - - //Wait until we are signalled or the time has elapsed - mre.WaitOne(milliseconds, false); - return m_eventNo; - } - - /// - /// Signals that an event has occurred and notifies all waiting threads - /// - public void SignalNewEvent() - { - lock (m_lock) - { - m_eventNo++; - while (m_waitQueue.Count > 0) - m_waitQueue.Dequeue().Set(); - } - - if (NewEvent != null) - NewEvent(this, null); - } - } -} +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; + +namespace Duplicati.Server +{ + /// + /// This class handles synchronized waiting for events + /// + public class EventPollNotify + { + /// + /// The lock that grants exclusive access to control structures + /// + private readonly object m_lock = new object(); + /// + /// The current eventID + /// + private long m_eventNo = 0; + /// + /// The list of subscribed waiting threads + /// + private readonly Queue m_waitQueue = new Queue(); + + /// + /// An eventhandler for subscribing to event updates without blocking + /// + public event EventHandler NewEvent; + + /// + /// Gets the current event ID + /// + public long EventNo { get { return m_eventNo; } } + + /// + /// Call to wait for an event that is newer than the current known event + /// + /// The last known event id + /// The number of milliseconds to block + /// The current event id + public long Wait(long eventId, int milliseconds) + { + System.Threading.ManualResetEvent mre; + lock (m_lock) + { + //If a newer event has already occured, return immediately + if (eventId != m_eventNo) + return m_eventNo; + + //Otherwise register this thread as waiting + mre = new System.Threading.ManualResetEvent(false); + m_waitQueue.Enqueue(mre); + } + + //Wait until we are signalled or the time has elapsed + mre.WaitOne(milliseconds, false); + return m_eventNo; + } + + /// + /// Signals that an event has occurred and notifies all waiting threads + /// + public void SignalNewEvent() + { + lock (m_lock) + { + m_eventNo++; + while (m_waitQueue.Count > 0) + m_waitQueue.Dequeue().Set(); + } + + if (NewEvent != null) + NewEvent(this, null); + } + } +} diff --git a/Duplicati.Library.RestAPI/FIXMEGlobal.cs b/Duplicati.Library.RestAPI/FIXMEGlobal.cs new file mode 100644 index 000000000..2a8fc3148 --- /dev/null +++ b/Duplicati.Library.RestAPI/FIXMEGlobal.cs @@ -0,0 +1,97 @@ + +using Duplicati.Server; +using System; +using System.Collections.Generic; + +namespace Duplicati.Library.RestAPI +{ + /** + * In the absense of dependancy injection, there is a significant amount of variables exposed through Program as globals. + * This causes a problem decoupling classes and leads to circular dependancies. + */ + public static class FIXMEGlobal + { + + /// + /// This is the only access to the database + /// + public static Server.Database.Connection DataConnection; + + /// + /// The controller interface for pause/resume and throttle options + /// + public static LiveControls LiveControl; + + /// + /// A delegate method for creating a copy of the current progress state + /// + public static Func GenerateProgressState; + + /// + /// The status event signaler, used to control long polling of status updates + /// + public static readonly EventPollNotify StatusEventNotifyer = new EventPollNotify(); + + /// + /// This is the working thread + /// + public static Duplicati.Library.Utility.WorkerThread WorkThread; + + public static Func PeekLastDataUpdateID; + public static Func PeekLastNotificationUpdateID; + + public static Action IncrementLastDataUpdateID; + + public static Action IncrementLastNotificationUpdateID; + + public static Action StartOrStopUsageReporter; + + public static Action UpdateThrottleSpeeds; + + /// + /// Gets the folder where Duplicati data is stored + /// + public static string DataFolder; + + /// + /// This is the scheduling thread + /// + public static Scheduler Scheduler; + + /// + /// The log redirect handler + /// + public static readonly LogWriteHandler LogHandler = new LogWriteHandler(); + + public static Func, Server.Database.Connection> GetDatabaseConnection; + + /// + /// The update poll thread. + /// + public static UpdatePollThread UpdatePoller; + + + /// + /// Used to check the origin of the web server (e.g. Tray icon or a stand alone Server) + /// + public static string Origin = "Server"; + + + /// + /// The application exit event + /// + public static System.Threading.ManualResetEvent ApplicationExitEvent; + + + /// + /// List of completed task results + /// + public static readonly List> TaskResultCache = new List>(); + + + /// + /// This is the lock to be used before manipulating the shared resources + /// + public static readonly object MainLock = new object(); + } +} diff --git a/Duplicati/Server/LiveControls.cs b/Duplicati.Library.RestAPI/LiveControls.cs similarity index 96% rename from Duplicati/Server/LiveControls.cs rename to Duplicati.Library.RestAPI/LiveControls.cs index bc261f57c..1a51636a3 100644 --- a/Duplicati/Server/LiveControls.cs +++ b/Duplicati.Library.RestAPI/LiveControls.cs @@ -1,403 +1,404 @@ -#region Disclaimer / License -// Copyright (C) 2015, The Duplicati Team -// http://www.duplicati.com, info@duplicati.com -// -// This library is free software; you can redistribute it and/or -// modify it under the terms of the GNU Lesser General Public -// License as published by the Free Software Foundation; either -// version 2.1 of the License, or (at your option) any later version. -// -// This library is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU -// Lesser General Public License for more details. -// -// You should have received a copy of the GNU Lesser General Public -// License along with this library; if not, write to the Free Software -// Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA -// -#endregion -using System; -using System.Collections.Generic; -using System.Text; +#region Disclaimer / License +// Copyright (C) 2015, The Duplicati Team +// http://www.duplicati.com, info@duplicati.com +// +// This library is free software; you can redistribute it and/or +// modify it under the terms of the GNU Lesser General Public +// License as published by the Free Software Foundation; either +// version 2.1 of the License, or (at your option) any later version. +// +// This library is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +// Lesser General Public License for more details. +// +// You should have received a copy of the GNU Lesser General Public +// License along with this library; if not, write to the Free Software +// Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA +// +#endregion +using System; +using System.Collections.Generic; +using System.Text; using Duplicati.Library.Common; +using Duplicati.Library.RestAPI; -namespace Duplicati.Server -{ - /// - /// This class keeps track of the users modifications regarding - /// throttling and pause/resume - /// - public class LiveControls - { - /// - /// The tag used for logging - /// - private static readonly string LOGTAG = Duplicati.Library.Logging.Log.LogTagFromType(); - - /// - /// An event that is activated when the pause state changes - /// - public event EventHandler StateChanged; - - /// - /// An event that is activated when the thread priority changes - /// - public event EventHandler ThreadPriorityChanged; - - /// - /// An event that is activated when the throttle speed changes - /// - public event EventHandler ThrottleSpeedChanged; - - /// - /// The possible states for the live control - /// - public enum LiveControlState - { - /// - /// Indicates that the backups are running - /// - Running, - /// - /// Indicates that the backups are currently suspended - /// - Paused - } - - /// - /// The current control state - /// - private LiveControlState m_state; - - /// - /// A value that indicates if the current pause state is caused by being suspended - /// - private bool m_pausedForSuspend = false; - - /// - /// The time to pause for, used to ensure that a user set pause can override the suspend pause - /// - private DateTime m_suspendMinimumPause = new DateTime(0); - - /// - /// Gets the current state for the control - /// - public LiveControlState State { get { return m_state; } } - - /// - /// The internal variable that tracks the the priority - /// - private System.Threading.ThreadPriority? m_priority; - - /// - /// The internal variable that tracks the upload limit - /// - private long? m_uploadLimit; - - /// - /// The internal variable that tracks the download limit - /// - private long? m_downloadLimit; - - /// - /// The object that ensures concurrent operations - /// - private readonly object m_lock = new object(); - - /// - /// Gets the current overridden thread priority - /// - public System.Threading.ThreadPriority? ThreadPriority - { - get { return m_priority; } - set - { - if (m_priority != value) - { - m_priority = value; - if (ThreadPriorityChanged != null) - ThreadPriorityChanged(this, null); - } - } - } - - /// - /// Gets the current upload limit in bps - /// - public long? UploadLimit - { - get { return m_uploadLimit; } - set - { - if (m_uploadLimit != value) - { - m_uploadLimit = value; - if (ThrottleSpeedChanged != null) - ThrottleSpeedChanged(this, null); - } - } - } - - /// - /// Gets the download limit in bps - /// - public long? DownloadLimit - { - get { return m_downloadLimit; } - set - { - if (m_downloadLimit != value) - { - m_downloadLimit = value; - if (ThrottleSpeedChanged != null) - ThrottleSpeedChanged(this, null); - } - } - } - - /// - /// The timer that is activated after a pause period. - /// - private readonly System.Threading.Timer m_waitTimer; - - /// - /// The time that the current pause is expected to expire - /// - private DateTime m_waitTimeExpiration = new DateTime(0); - - /// - /// Constructs a new instance of the LiveControl - /// - public LiveControls(Database.ServerSettings settings) - { - m_state = LiveControlState.Running; - m_waitTimer = new System.Threading.Timer(m_waitTimer_Tick, this, System.Threading.Timeout.Infinite, System.Threading.Timeout.Infinite); - - if (!string.IsNullOrEmpty(settings.StartupDelayDuration) && settings.StartupDelayDuration != "0") - { - long milliseconds = 0; - try { milliseconds = (long)Duplicati.Library.Utility.Timeparser.ParseTimeSpan(settings.StartupDelayDuration).TotalMilliseconds; } - catch {} - - if (milliseconds > 0) - { - m_waitTimeExpiration = DateTime.Now.AddMilliseconds(milliseconds); - m_waitTimer.Change(milliseconds, System.Threading.Timeout.Infinite); - m_state = LiveControlState.Paused; - } - } - - m_priority = settings.ThreadPriorityOverride; - if (!string.IsNullOrEmpty(settings.DownloadSpeedLimit)) - try - { - m_downloadLimit = Library.Utility.Sizeparser.ParseSize(settings.DownloadSpeedLimit, "kb"); - } - catch (Exception ex) - { - Library.Logging.Log.WriteErrorMessage(LOGTAG, "ParseDownloadLimitError", ex, "Failed to parse download limit: {0}", settings.DownloadSpeedLimit); - } - - if (!string.IsNullOrEmpty(settings.UploadSpeedLimit)) - try - { - m_uploadLimit = Library.Utility.Sizeparser.ParseSize(settings.UploadSpeedLimit, "kb"); - } - catch (Exception ex) - { - Library.Logging.Log.WriteErrorMessage(LOGTAG, "ParseUploadLimitError", ex, "Failed to parse upload limit: {0}", settings.UploadSpeedLimit); - } - - try - { - if (!Platform.IsClientPosix) - RegisterHibernateMonitor(); - } - catch { } - } - - /// - /// Event that occurs when the timeout duration is exceeded - /// - /// The sender of the event - private void m_waitTimer_Tick(object sender) - { - lock (m_lock) - Resume(); - } - - /// - /// Internal helper to reset the timeout timer - /// - /// The time to wait - private void ResetTimer(string timeout) - { - lock (m_lock) - if (!string.IsNullOrEmpty(timeout)) - { - long milliseconds = (long)Duplicati.Library.Utility.Timeparser.ParseTimeSpan(timeout).TotalMilliseconds; - m_waitTimeExpiration = DateTime.Now.AddMilliseconds(milliseconds); - m_waitTimer.Change(milliseconds, System.Threading.Timeout.Infinite); - } - else - { - m_waitTimeExpiration = new DateTime(0); - m_waitTimer.Change(System.Threading.Timeout.Infinite, System.Threading.Timeout.Infinite); - } - } - - /// - /// Internal helper to set the pause mode - /// - private void SetPauseMode() - { - lock (m_lock) - { - if (m_state == LiveControlState.Running) - { - m_state = LiveControlState.Paused; - if (StateChanged != null) - StateChanged(this, null); - } - } - } - - /// - /// Pauses the backups until resumed - /// - public void Pause() - { - lock(m_lock) - { - var fireEvent = m_waitTimeExpiration.Ticks != 0 && m_state == LiveControlState.Paused && StateChanged != null; - - ResetTimer(null); - - if (fireEvent) - StateChanged(this, null); - else - SetPauseMode(); - } - } - - /// - /// Resumes a backups to the running state - /// - public void Resume() - { - lock (m_lock) - { - if (m_state == LiveControlState.Paused) - { - //Make sure that the timer is cleared - ResetTimer(null); - - m_state = LiveControlState.Running; - if (StateChanged != null) - StateChanged(this, null); - } - } - } - - /// - /// Suspends the backups for a given period - /// - /// The duration to wait - public void Pause(string timeout) - { - Pause(Duplicati.Library.Utility.Timeparser.ParseTimeSpan(timeout)); - } - - /// - /// Suspends the backups for a given period - /// - /// The duration to wait - public void Pause(TimeSpan timeout) - { - lock (m_lock) - { - m_waitTimeExpiration = DateTime.Now.AddMilliseconds((long)timeout.TotalMilliseconds); - m_waitTimer.Change((long)timeout.TotalMilliseconds, System.Threading.Timeout.Infinite); - - //We change the time, so we issue a new event - if (m_state == LiveControlState.Paused && StateChanged != null) - StateChanged(this, null); - else - SetPauseMode(); - } - } - - /// - /// Gets the time the current pause is expected to end - /// - public DateTime EstimatedPauseEnd { get { return m_waitTimeExpiration; } } - - /// - /// Method for calling a Win32 API - /// - private void RegisterHibernateMonitor() - { - Microsoft.Win32.SystemEvents.PowerModeChanged += new Microsoft.Win32.PowerModeChangedEventHandler(SystemEvents_PowerModeChanged); - } - - /// - /// A monitor for detecting when the system hibernates or resumes - /// - /// Unused sender parameter - /// The event information - private void SystemEvents_PowerModeChanged(object sender, object _e) - { - Microsoft.Win32.PowerModeChangedEventArgs e = _e as Microsoft.Win32.PowerModeChangedEventArgs; - if (e == null) - return; - - if (e.Mode == Microsoft.Win32.PowerModes.Suspend) - { - //If we are running, register as being paused due to suspending - if (this.m_state == LiveControlState.Running) - { - this.SetPauseMode(); - m_pausedForSuspend = true; - m_suspendMinimumPause = new DateTime(0); - } - else - { - if (m_waitTimeExpiration.Ticks != 0) - { - m_pausedForSuspend = true; - m_suspendMinimumPause = this.EstimatedPauseEnd; - ResetTimer(null); - } - - } - } - else if (e.Mode == Microsoft.Win32.PowerModes.Resume) - { - //If we have been been paused due to suspending, we un-pause now - if (m_pausedForSuspend) - { - long delayTicks = (m_suspendMinimumPause - DateTime.Now).Ticks; - - var appset = Program.DataConnection.ApplicationSettings; - if (!string.IsNullOrEmpty(appset.StartupDelayDuration) && appset.StartupDelayDuration != "0") - try { delayTicks = Math.Max(delayTicks, Library.Utility.Timeparser.ParseTimeSpan(appset.StartupDelayDuration).Ticks); } - catch { } - - if (delayTicks > 0) - { - this.Pause(TimeSpan.FromTicks(delayTicks)); - } - else - { - this.Resume(); - } - } - - m_pausedForSuspend = false; - m_suspendMinimumPause = new DateTime(0); - } - } - - } -} +namespace Duplicati.Server +{ + /// + /// This class keeps track of the users modifications regarding + /// throttling and pause/resume + /// + public class LiveControls + { + /// + /// The tag used for logging + /// + private static readonly string LOGTAG = Duplicati.Library.Logging.Log.LogTagFromType(); + + /// + /// An event that is activated when the pause state changes + /// + public event EventHandler StateChanged; + + /// + /// An event that is activated when the thread priority changes + /// + public event EventHandler ThreadPriorityChanged; + + /// + /// An event that is activated when the throttle speed changes + /// + public event EventHandler ThrottleSpeedChanged; + + /// + /// The possible states for the live control + /// + public enum LiveControlState + { + /// + /// Indicates that the backups are running + /// + Running, + /// + /// Indicates that the backups are currently suspended + /// + Paused + } + + /// + /// The current control state + /// + private LiveControlState m_state; + + /// + /// A value that indicates if the current pause state is caused by being suspended + /// + private bool m_pausedForSuspend = false; + + /// + /// The time to pause for, used to ensure that a user set pause can override the suspend pause + /// + private DateTime m_suspendMinimumPause = new DateTime(0); + + /// + /// Gets the current state for the control + /// + public LiveControlState State { get { return m_state; } } + + /// + /// The internal variable that tracks the the priority + /// + private System.Threading.ThreadPriority? m_priority; + + /// + /// The internal variable that tracks the upload limit + /// + private long? m_uploadLimit; + + /// + /// The internal variable that tracks the download limit + /// + private long? m_downloadLimit; + + /// + /// The object that ensures concurrent operations + /// + private readonly object m_lock = new object(); + + /// + /// Gets the current overridden thread priority + /// + public System.Threading.ThreadPriority? ThreadPriority + { + get { return m_priority; } + set + { + if (m_priority != value) + { + m_priority = value; + if (ThreadPriorityChanged != null) + ThreadPriorityChanged(this, null); + } + } + } + + /// + /// Gets the current upload limit in bps + /// + public long? UploadLimit + { + get { return m_uploadLimit; } + set + { + if (m_uploadLimit != value) + { + m_uploadLimit = value; + if (ThrottleSpeedChanged != null) + ThrottleSpeedChanged(this, null); + } + } + } + + /// + /// Gets the download limit in bps + /// + public long? DownloadLimit + { + get { return m_downloadLimit; } + set + { + if (m_downloadLimit != value) + { + m_downloadLimit = value; + if (ThrottleSpeedChanged != null) + ThrottleSpeedChanged(this, null); + } + } + } + + /// + /// The timer that is activated after a pause period. + /// + private readonly System.Threading.Timer m_waitTimer; + + /// + /// The time that the current pause is expected to expire + /// + private DateTime m_waitTimeExpiration = new DateTime(0); + + /// + /// Constructs a new instance of the LiveControl + /// + public LiveControls(Database.ServerSettings settings) + { + m_state = LiveControlState.Running; + m_waitTimer = new System.Threading.Timer(m_waitTimer_Tick, this, System.Threading.Timeout.Infinite, System.Threading.Timeout.Infinite); + + if (!string.IsNullOrEmpty(settings.StartupDelayDuration) && settings.StartupDelayDuration != "0") + { + long milliseconds = 0; + try { milliseconds = (long)Duplicati.Library.Utility.Timeparser.ParseTimeSpan(settings.StartupDelayDuration).TotalMilliseconds; } + catch {} + + if (milliseconds > 0) + { + m_waitTimeExpiration = DateTime.Now.AddMilliseconds(milliseconds); + m_waitTimer.Change(milliseconds, System.Threading.Timeout.Infinite); + m_state = LiveControlState.Paused; + } + } + + m_priority = settings.ThreadPriorityOverride; + if (!string.IsNullOrEmpty(settings.DownloadSpeedLimit)) + try + { + m_downloadLimit = Library.Utility.Sizeparser.ParseSize(settings.DownloadSpeedLimit, "kb"); + } + catch (Exception ex) + { + Library.Logging.Log.WriteErrorMessage(LOGTAG, "ParseDownloadLimitError", ex, "Failed to parse download limit: {0}", settings.DownloadSpeedLimit); + } + + if (!string.IsNullOrEmpty(settings.UploadSpeedLimit)) + try + { + m_uploadLimit = Library.Utility.Sizeparser.ParseSize(settings.UploadSpeedLimit, "kb"); + } + catch (Exception ex) + { + Library.Logging.Log.WriteErrorMessage(LOGTAG, "ParseUploadLimitError", ex, "Failed to parse upload limit: {0}", settings.UploadSpeedLimit); + } + + try + { + if (!Platform.IsClientPosix) + RegisterHibernateMonitor(); + } + catch { } + } + + /// + /// Event that occurs when the timeout duration is exceeded + /// + /// The sender of the event + private void m_waitTimer_Tick(object sender) + { + lock (m_lock) + Resume(); + } + + /// + /// Internal helper to reset the timeout timer + /// + /// The time to wait + private void ResetTimer(string timeout) + { + lock (m_lock) + if (!string.IsNullOrEmpty(timeout)) + { + long milliseconds = (long)Duplicati.Library.Utility.Timeparser.ParseTimeSpan(timeout).TotalMilliseconds; + m_waitTimeExpiration = DateTime.Now.AddMilliseconds(milliseconds); + m_waitTimer.Change(milliseconds, System.Threading.Timeout.Infinite); + } + else + { + m_waitTimeExpiration = new DateTime(0); + m_waitTimer.Change(System.Threading.Timeout.Infinite, System.Threading.Timeout.Infinite); + } + } + + /// + /// Internal helper to set the pause mode + /// + private void SetPauseMode() + { + lock (m_lock) + { + if (m_state == LiveControlState.Running) + { + m_state = LiveControlState.Paused; + if (StateChanged != null) + StateChanged(this, null); + } + } + } + + /// + /// Pauses the backups until resumed + /// + public void Pause() + { + lock(m_lock) + { + var fireEvent = m_waitTimeExpiration.Ticks != 0 && m_state == LiveControlState.Paused && StateChanged != null; + + ResetTimer(null); + + if (fireEvent) + StateChanged(this, null); + else + SetPauseMode(); + } + } + + /// + /// Resumes a backups to the running state + /// + public void Resume() + { + lock (m_lock) + { + if (m_state == LiveControlState.Paused) + { + //Make sure that the timer is cleared + ResetTimer(null); + + m_state = LiveControlState.Running; + if (StateChanged != null) + StateChanged(this, null); + } + } + } + + /// + /// Suspends the backups for a given period + /// + /// The duration to wait + public void Pause(string timeout) + { + Pause(Duplicati.Library.Utility.Timeparser.ParseTimeSpan(timeout)); + } + + /// + /// Suspends the backups for a given period + /// + /// The duration to wait + public void Pause(TimeSpan timeout) + { + lock (m_lock) + { + m_waitTimeExpiration = DateTime.Now.AddMilliseconds((long)timeout.TotalMilliseconds); + m_waitTimer.Change((long)timeout.TotalMilliseconds, System.Threading.Timeout.Infinite); + + //We change the time, so we issue a new event + if (m_state == LiveControlState.Paused && StateChanged != null) + StateChanged(this, null); + else + SetPauseMode(); + } + } + + /// + /// Gets the time the current pause is expected to end + /// + public DateTime EstimatedPauseEnd { get { return m_waitTimeExpiration; } } + + /// + /// Method for calling a Win32 API + /// + private void RegisterHibernateMonitor() + { + Microsoft.Win32.SystemEvents.PowerModeChanged += new Microsoft.Win32.PowerModeChangedEventHandler(SystemEvents_PowerModeChanged); + } + + /// + /// A monitor for detecting when the system hibernates or resumes + /// + /// Unused sender parameter + /// The event information + private void SystemEvents_PowerModeChanged(object sender, object _e) + { + Microsoft.Win32.PowerModeChangedEventArgs e = _e as Microsoft.Win32.PowerModeChangedEventArgs; + if (e == null) + return; + + if (e.Mode == Microsoft.Win32.PowerModes.Suspend) + { + //If we are running, register as being paused due to suspending + if (this.m_state == LiveControlState.Running) + { + this.SetPauseMode(); + m_pausedForSuspend = true; + m_suspendMinimumPause = new DateTime(0); + } + else + { + if (m_waitTimeExpiration.Ticks != 0) + { + m_pausedForSuspend = true; + m_suspendMinimumPause = this.EstimatedPauseEnd; + ResetTimer(null); + } + + } + } + else if (e.Mode == Microsoft.Win32.PowerModes.Resume) + { + //If we have been been paused due to suspending, we un-pause now + if (m_pausedForSuspend) + { + long delayTicks = (m_suspendMinimumPause - DateTime.Now).Ticks; + + var appset = FIXMEGlobal.DataConnection.ApplicationSettings; + if (!string.IsNullOrEmpty(appset.StartupDelayDuration) && appset.StartupDelayDuration != "0") + try { delayTicks = Math.Max(delayTicks, Library.Utility.Timeparser.ParseTimeSpan(appset.StartupDelayDuration).Ticks); } + catch { } + + if (delayTicks > 0) + { + this.Pause(TimeSpan.FromTicks(delayTicks)); + } + else + { + this.Resume(); + } + } + + m_pausedForSuspend = false; + m_suspendMinimumPause = new DateTime(0); + } + } + + } +} diff --git a/Duplicati/Server/LogWriteHandler.cs b/Duplicati.Library.RestAPI/LogWriteHandler.cs similarity index 97% rename from Duplicati/Server/LogWriteHandler.cs rename to Duplicati.Library.RestAPI/LogWriteHandler.cs index 640886f66..1f84c8bbb 100644 --- a/Duplicati/Server/LogWriteHandler.cs +++ b/Duplicati.Library.RestAPI/LogWriteHandler.cs @@ -1,351 +1,351 @@ -// Copyright (C) 2015, The Duplicati Team - -// http://www.duplicati.com, info@duplicati.com -// -// This library is free software; you can redistribute it and/or modify -// it under the terms of the GNU Lesser General Public License as -// published by the Free Software Foundation; either version 2.1 of the -// License, or (at your option) any later version. -// -// This library is distributed in the hope that it will be useful, but -// WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU -// Lesser General Public License for more details. -// -// You should have received a copy of the GNU Lesser General Public -// License along with this library; if not, write to the Free Software -// Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA -using System; -using System.Linq; -using Duplicati.Library.Logging; -using System.Collections.Generic; -using Duplicati.Library.Interface; - -namespace Duplicati.Server -{ - /// - /// Class that handles logging from the server, - /// and provides an entry point for the runner - /// to redirect log output to a file - /// - public class LogWriteHandler : ILogDestination, IDisposable - { - /// - /// The number of messages to keep when inactive - /// - private const int INACTIVE_SIZE = 30; - /// - /// The number of messages to keep when active - /// - private const int ACTIVE_SIZE = 5000; - - /// - /// The context key used for conveying the backup ID - /// - public const string LOG_EXTRA_BACKUPID = "BackupID"; - /// - /// The context key used for conveying the task ID - /// - public const string LOG_EXTRA_TASKID = "TaskID"; - - /// - /// Represents a single log event - /// - public struct LogEntry - { - /// - /// A unique ID that sequentially increments - /// - private static long _id; - - /// - /// The time the message was logged - /// - public readonly DateTime When; - - /// - /// The ID assigned to the message - /// - public readonly long ID; - - /// - /// The logged message - /// - public readonly string Message; - - /// - /// The log tag - /// - public readonly string Tag; - - /// - /// The message ID - /// - public readonly string MessageID; - - /// - /// The message ID - /// - public readonly string ExceptionID; - - /// - /// The message type - /// - public readonly LogMessageType Type; - - /// - /// Exception data attached to the message - /// - public readonly Exception Exception; - - /// - /// The backup ID, if any - /// - public readonly string BackupID; - - /// - /// The task ID, if any - /// - public readonly string TaskID; - - /// - /// Initializes a new instance of the struct. - /// - /// The log entry to store - public LogEntry(Duplicati.Library.Logging.LogEntry entry) - { - this.ID = System.Threading.Interlocked.Increment(ref _id); - this.When = entry.When; - this.Message = entry.FormattedMessage; - this.Type = entry.Level; - this.Exception = entry.Exception; - this.Tag = entry.FilterTag; - this.MessageID = entry.Id; - this.BackupID = entry[LOG_EXTRA_BACKUPID]; - this.TaskID = entry[LOG_EXTRA_TASKID]; - - if (entry.Exception == null) - this.ExceptionID = null; - else if (entry.Exception is UserInformationException exception) - this.ExceptionID = exception.HelpID; - else - this.ExceptionID = entry.Exception.GetType().FullName; - - } - } - - /// - /// Basic implementation of a ring-buffer - /// - private class RingBuffer : IEnumerable - { - private readonly T[] m_buffer; - private int m_head; - private int m_tail; - private int m_length; - private int m_key; - private readonly object m_lock = new object(); - - public RingBuffer(int size, IEnumerable initial = null) - { - m_buffer = new T[size]; - if (initial != null) - foreach(var t in initial) - this.Enqueue(t); - } - - public int Length { get { return m_length; } } - - public void Enqueue(T item) - { - lock(m_lock) - { - m_key++; - m_buffer[m_head] = item; - m_head = (m_head + 1) % m_buffer.Length; - if (m_length == m_buffer.Length) - m_tail = (m_tail + 1) % m_buffer.Length; - else - m_length++; - } - } - - #region IEnumerable implementation - public IEnumerator GetEnumerator() - { - var k = m_key; - for(var i = 0; i < m_length; i++) - if (m_key != k) - throw new InvalidOperationException("Buffer was modified while reading"); - else - yield return m_buffer[(m_tail + i) % m_buffer.Length]; - } - #endregion - #region IEnumerable implementation - System.Collections.IEnumerator System.Collections.IEnumerable.GetEnumerator() - { - return GetEnumerator(); - } - #endregion - - public T[] FlatArray(Func filter = null) - { - lock(m_lock) - if (filter == null) - return this.ToArray(); - else - return this.Where(filter).ToArray(); - } - - public int Size { get { return m_buffer.Length; } } - } - - private readonly DateTime[] m_timeouts; - private readonly object m_lock = new object(); - private volatile bool m_anytimeouts = false; - private RingBuffer m_buffer; - - private ILogDestination m_serverfile; - private LogMessageType m_serverloglevel; - private LogMessageType m_logLevel; - - public LogWriteHandler() - { - var fields = Enum.GetValues(typeof(LogMessageType)); - m_timeouts = new DateTime[fields.Length]; - m_buffer = new RingBuffer(INACTIVE_SIZE); - } - - public void RenewTimeout(LogMessageType type) - { - lock(m_lock) - { - m_timeouts[(int)type] = DateTime.Now.AddSeconds(30); - m_anytimeouts = true; - if (m_buffer == null || m_buffer.Size == INACTIVE_SIZE) - m_buffer = new RingBuffer(ACTIVE_SIZE, m_buffer); - } - } - - public void SetServerFile(string path, LogMessageType level) - { - var dir = System.IO.Path.GetDirectoryName(System.IO.Path.GetFullPath(path)); - if (!System.IO.Directory.Exists(dir)) - System.IO.Directory.CreateDirectory(dir); - - m_serverfile = new StreamLogDestination(path); - m_serverloglevel = level; - - UpdateLogLevel(); - } - - public LogEntry[] AfterTime(DateTime offset, LogMessageType level) - { - RenewTimeout(level); - UpdateLogLevel(); - - offset = offset.ToUniversalTime(); - lock(m_lock) - { - if (m_buffer == null) - return new LogEntry[0]; - - return m_buffer.FlatArray((x) => x.When > offset && x.Type >= level ); - } - } - - public LogEntry[] AfterID(long id, LogMessageType level, int pagesize) - { - RenewTimeout(level); - UpdateLogLevel(); - - lock(m_lock) - { - if (m_buffer == null) - return new LogEntry[0]; - - var buffer = m_buffer.FlatArray((x) => x.ID > id && x.Type >= level ); - // Return the newest entries - if (buffer.Length > pagesize) { - var index = buffer.Length - pagesize; - return buffer.Skip(index).Take(pagesize).ToArray(); - } - else { - return buffer; - } - } - } - - private int[] GetActiveTimeouts() - { - var i = 0; - return (from n in m_timeouts - let ix = i++ - where n > DateTime.Now - select ix).ToArray(); - } - - private void UpdateLogLevel() - { - m_logLevel = - (LogMessageType)(GetActiveTimeouts().Union(new int[] { (int)m_serverloglevel }).Min()); - } - - - #region ILog implementation - - public void WriteMessage(Duplicati.Library.Logging.LogEntry entry) - { - if (entry.Level < m_logLevel) - return; - - if (m_serverfile != null && entry.Level >= m_serverloglevel) - try - { - m_serverfile.WriteMessage(entry); - } - catch - { - } - - lock(m_lock) - { - if (m_anytimeouts) - { - var q = GetActiveTimeouts(); - - if (q.Length == 0) - { - UpdateLogLevel(); - m_anytimeouts = false; - if (m_buffer == null || m_buffer.Size != INACTIVE_SIZE) - m_buffer = new RingBuffer(INACTIVE_SIZE, m_buffer); - - } - } - - if (m_buffer != null) - m_buffer.Enqueue(new LogEntry(entry)); - } - - } - - #endregion - - #region IDisposable implementation - - public void Dispose() - { - if (m_serverfile != null) - { - var sf = m_serverfile; - m_serverfile = null; - if (sf is IDisposable disposable) - disposable.Dispose(); - } - } - - #endregion - } -} - +// Copyright (C) 2015, The Duplicati Team + +// http://www.duplicati.com, info@duplicati.com +// +// This library is free software; you can redistribute it and/or modify +// it under the terms of the GNU Lesser General Public License as +// published by the Free Software Foundation; either version 2.1 of the +// License, or (at your option) any later version. +// +// This library is distributed in the hope that it will be useful, but +// WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +// Lesser General Public License for more details. +// +// You should have received a copy of the GNU Lesser General Public +// License along with this library; if not, write to the Free Software +// Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA +using System; +using System.Linq; +using Duplicati.Library.Logging; +using System.Collections.Generic; +using Duplicati.Library.Interface; + +namespace Duplicati.Server +{ + /// + /// Class that handles logging from the server, + /// and provides an entry point for the runner + /// to redirect log output to a file + /// + public class LogWriteHandler : ILogDestination, IDisposable + { + /// + /// The number of messages to keep when inactive + /// + private const int INACTIVE_SIZE = 30; + /// + /// The number of messages to keep when active + /// + private const int ACTIVE_SIZE = 5000; + + /// + /// The context key used for conveying the backup ID + /// + public const string LOG_EXTRA_BACKUPID = "BackupID"; + /// + /// The context key used for conveying the task ID + /// + public const string LOG_EXTRA_TASKID = "TaskID"; + + /// + /// Represents a single log event + /// + public struct LogEntry + { + /// + /// A unique ID that sequentially increments + /// + private static long _id; + + /// + /// The time the message was logged + /// + public readonly DateTime When; + + /// + /// The ID assigned to the message + /// + public readonly long ID; + + /// + /// The logged message + /// + public readonly string Message; + + /// + /// The log tag + /// + public readonly string Tag; + + /// + /// The message ID + /// + public readonly string MessageID; + + /// + /// The message ID + /// + public readonly string ExceptionID; + + /// + /// The message type + /// + public readonly LogMessageType Type; + + /// + /// Exception data attached to the message + /// + public readonly Exception Exception; + + /// + /// The backup ID, if any + /// + public readonly string BackupID; + + /// + /// The task ID, if any + /// + public readonly string TaskID; + + /// + /// Initializes a new instance of the struct. + /// + /// The log entry to store + public LogEntry(Duplicati.Library.Logging.LogEntry entry) + { + this.ID = System.Threading.Interlocked.Increment(ref _id); + this.When = entry.When; + this.Message = entry.FormattedMessage; + this.Type = entry.Level; + this.Exception = entry.Exception; + this.Tag = entry.FilterTag; + this.MessageID = entry.Id; + this.BackupID = entry[LOG_EXTRA_BACKUPID]; + this.TaskID = entry[LOG_EXTRA_TASKID]; + + if (entry.Exception == null) + this.ExceptionID = null; + else if (entry.Exception is UserInformationException exception) + this.ExceptionID = exception.HelpID; + else + this.ExceptionID = entry.Exception.GetType().FullName; + + } + } + + /// + /// Basic implementation of a ring-buffer + /// + private class RingBuffer : IEnumerable + { + private readonly T[] m_buffer; + private int m_head; + private int m_tail; + private int m_length; + private int m_key; + private readonly object m_lock = new object(); + + public RingBuffer(int size, IEnumerable initial = null) + { + m_buffer = new T[size]; + if (initial != null) + foreach(var t in initial) + this.Enqueue(t); + } + + public int Length { get { return m_length; } } + + public void Enqueue(T item) + { + lock(m_lock) + { + m_key++; + m_buffer[m_head] = item; + m_head = (m_head + 1) % m_buffer.Length; + if (m_length == m_buffer.Length) + m_tail = (m_tail + 1) % m_buffer.Length; + else + m_length++; + } + } + + #region IEnumerable implementation + public IEnumerator GetEnumerator() + { + var k = m_key; + for(var i = 0; i < m_length; i++) + if (m_key != k) + throw new InvalidOperationException("Buffer was modified while reading"); + else + yield return m_buffer[(m_tail + i) % m_buffer.Length]; + } + #endregion + #region IEnumerable implementation + System.Collections.IEnumerator System.Collections.IEnumerable.GetEnumerator() + { + return GetEnumerator(); + } + #endregion + + public T[] FlatArray(Func filter = null) + { + lock(m_lock) + if (filter == null) + return this.ToArray(); + else + return this.Where(filter).ToArray(); + } + + public int Size { get { return m_buffer.Length; } } + } + + private readonly DateTime[] m_timeouts; + private readonly object m_lock = new object(); + private volatile bool m_anytimeouts = false; + private RingBuffer m_buffer; + + private ILogDestination m_serverfile; + private LogMessageType m_serverloglevel; + private LogMessageType m_logLevel; + + public LogWriteHandler() + { + var fields = Enum.GetValues(typeof(LogMessageType)); + m_timeouts = new DateTime[fields.Length]; + m_buffer = new RingBuffer(INACTIVE_SIZE); + } + + public void RenewTimeout(LogMessageType type) + { + lock(m_lock) + { + m_timeouts[(int)type] = DateTime.Now.AddSeconds(30); + m_anytimeouts = true; + if (m_buffer == null || m_buffer.Size == INACTIVE_SIZE) + m_buffer = new RingBuffer(ACTIVE_SIZE, m_buffer); + } + } + + public void SetServerFile(string path, LogMessageType level) + { + var dir = System.IO.Path.GetDirectoryName(System.IO.Path.GetFullPath(path)); + if (!System.IO.Directory.Exists(dir)) + System.IO.Directory.CreateDirectory(dir); + + m_serverfile = new StreamLogDestination(path); + m_serverloglevel = level; + + UpdateLogLevel(); + } + + public LogEntry[] AfterTime(DateTime offset, LogMessageType level) + { + RenewTimeout(level); + UpdateLogLevel(); + + offset = offset.ToUniversalTime(); + lock(m_lock) + { + if (m_buffer == null) + return new LogEntry[0]; + + return m_buffer.FlatArray((x) => x.When > offset && x.Type >= level ); + } + } + + public LogEntry[] AfterID(long id, LogMessageType level, int pagesize) + { + RenewTimeout(level); + UpdateLogLevel(); + + lock(m_lock) + { + if (m_buffer == null) + return new LogEntry[0]; + + var buffer = m_buffer.FlatArray((x) => x.ID > id && x.Type >= level ); + // Return the newest entries + if (buffer.Length > pagesize) { + var index = buffer.Length - pagesize; + return buffer.Skip(index).Take(pagesize).ToArray(); + } + else { + return buffer; + } + } + } + + private int[] GetActiveTimeouts() + { + var i = 0; + return (from n in m_timeouts + let ix = i++ + where n > DateTime.Now + select ix).ToArray(); + } + + private void UpdateLogLevel() + { + m_logLevel = + (LogMessageType)(GetActiveTimeouts().Union(new int[] { (int)m_serverloglevel }).Min()); + } + + + #region ILog implementation + + public void WriteMessage(Duplicati.Library.Logging.LogEntry entry) + { + if (entry.Level < m_logLevel) + return; + + if (m_serverfile != null && entry.Level >= m_serverloglevel) + try + { + m_serverfile.WriteMessage(entry); + } + catch + { + } + + lock(m_lock) + { + if (m_anytimeouts) + { + var q = GetActiveTimeouts(); + + if (q.Length == 0) + { + UpdateLogLevel(); + m_anytimeouts = false; + if (m_buffer == null || m_buffer.Size != INACTIVE_SIZE) + m_buffer = new RingBuffer(INACTIVE_SIZE, m_buffer); + + } + } + + if (m_buffer != null) + m_buffer.Enqueue(new LogEntry(entry)); + } + + } + + #endregion + + #region IDisposable implementation + + public void Dispose() + { + if (m_serverfile != null) + { + var sf = m_serverfile; + m_serverfile = null; + if (sf is IDisposable disposable) + disposable.Dispose(); + } + } + + #endregion + } +} + diff --git a/Duplicati/Server/WebServer/RESTMethods/Acknowledgements.cs b/Duplicati.Library.RestAPI/RESTMethods/Acknowledgements.cs similarity index 97% rename from Duplicati/Server/WebServer/RESTMethods/Acknowledgements.cs rename to Duplicati.Library.RestAPI/RESTMethods/Acknowledgements.cs index 6ca1bd459..488cb7456 100644 --- a/Duplicati/Server/WebServer/RESTMethods/Acknowledgements.cs +++ b/Duplicati.Library.RestAPI/RESTMethods/Acknowledgements.cs @@ -1,53 +1,53 @@ -// Copyright (C) 2015, The Duplicati Team -// http://www.duplicati.com, info@duplicati.com -// -// This library is free software; you can redistribute it and/or modify -// it under the terms of the GNU Lesser General Public License as -// published by the Free Software Foundation; either version 2.1 of the -// License, or (at your option) any later version. -// -// This library is distributed in the hope that it will be useful, but -// WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU -// Lesser General Public License for more details. -// -// You should have received a copy of the GNU Lesser General Public -// License along with this library; if not, write to the Free Software -// Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA -using System; -using System.Collections.Generic; +// Copyright (C) 2015, The Duplicati Team +// http://www.duplicati.com, info@duplicati.com +// +// This library is free software; you can redistribute it and/or modify +// it under the terms of the GNU Lesser General Public License as +// published by the Free Software Foundation; either version 2.1 of the +// License, or (at your option) any later version. +// +// This library is distributed in the hope that it will be useful, but +// WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +// Lesser General Public License for more details. +// +// You should have received a copy of the GNU Lesser General Public +// License along with this library; if not, write to the Free Software +// Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA +using System; +using System.Collections.Generic; using Duplicati.Library.Common.IO; using Duplicati.Library.Utility; -namespace Duplicati.Server.WebServer.RESTMethods -{ - public class Acknowledgements : IRESTMethodGET, IRESTMethodDocumented - { - private class GetResponse - { - public string Status; - public string Acknowledgements; - } - - public void GET(string key, RequestInfo info) - { - var path = SystemIO.IO_OS.PathCombine(System.IO.Path.GetDirectoryName(System.Reflection.Assembly.GetExecutingAssembly().Location), "acknowledgements.txt"); - info.OutputOK(new GetResponse() { - Status = "OK", - Acknowledgements = System.IO.File.ReadAllText(path) - }); - } - public string Description { get { return "Gets all acknowledgements"; } } - - public IEnumerable> Types - { - get - { - return new KeyValuePair[] { - new KeyValuePair(HttpServer.Method.Get, typeof(GetResponse)), - }; - } - } - } -} - +namespace Duplicati.Server.WebServer.RESTMethods +{ + public class Acknowledgements : IRESTMethodGET, IRESTMethodDocumented + { + private class GetResponse + { + public string Status; + public string Acknowledgements; + } + + public void GET(string key, RequestInfo info) + { + var path = SystemIO.IO_OS.PathCombine(System.IO.Path.GetDirectoryName(System.Reflection.Assembly.GetExecutingAssembly().Location), "acknowledgements.txt"); + info.OutputOK(new GetResponse() { + Status = "OK", + Acknowledgements = System.IO.File.ReadAllText(path) + }); + } + public string Description { get { return "Gets all acknowledgements"; } } + + public IEnumerable> Types + { + get + { + return new KeyValuePair[] { + new KeyValuePair(HttpServer.Method.Get, typeof(GetResponse)), + }; + } + } + } +} + diff --git a/Duplicati/Server/WebServer/RESTMethods/Backup.cs b/Duplicati.Library.RestAPI/RESTMethods/Backup.cs similarity index 87% rename from Duplicati/Server/WebServer/RESTMethods/Backup.cs rename to Duplicati.Library.RestAPI/RESTMethods/Backup.cs index 926e7ba80..232c428a2 100644 --- a/Duplicati/Server/WebServer/RESTMethods/Backup.cs +++ b/Duplicati.Library.RestAPI/RESTMethods/Backup.cs @@ -1,732 +1,733 @@ -// Copyright (C) 2015, The Duplicati Team -// http://www.duplicati.com, info@duplicati.com -// -// This library is free software; you can redistribute it and/or modify -// it under the terms of the GNU Lesser General Public License as -// published by the Free Software Foundation; either version 2.1 of the -// License, or (at your option) any later version. -// -// This library is distributed in the hope that it will be useful, but -// WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU -// Lesser General Public License for more details. -// -// You should have received a copy of the GNU Lesser General Public -// License along with this library; if not, write to the Free Software -// Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA -using System; -using System.Collections.Generic; -using Duplicati.Server.Serialization; -using System.IO; -using System.Linq; -using Duplicati.Server.Serialization.Interface; - -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 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(); - - 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 - { - ["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 = Program.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())) - { - 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); - - Program.WorkThread.AddTask(task); - - info.OutputOK(new { TaskID = task.TaskID }); - } - - private void CreateReport(IBackup backup, RequestInfo info) - { - var task = Runner.CreateTask(DuplicatiOperation.CreateReport, backup); - Program.WorkThread.AddTask(task); - Program.StatusEventNotifyer.SignalNewEvent(); - - info.OutputOK(new { Status = "OK", ID = task.TaskID }); - } - - private void ReportRemoteSize(IBackup backup, RequestInfo info) - { - var task = Runner.CreateTask(DuplicatiOperation.ListRemote, backup); - Program.WorkThread.AddTask(task); - Program.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); - Program.WorkThread.AddTask(task); - Program.StatusEventNotifyer.SignalNewEvent(); - - info.OutputOK(new { Status = "OK", ID = task.TaskID }); - } - - private void Verify(IBackup backup, RequestInfo info) - { - var task = Runner.CreateTask(DuplicatiOperation.Verify, backup); - Program.WorkThread.AddTask(task); - Program.StatusEventNotifyer.SignalNewEvent(); - - info.OutputOK(new {Status = "OK", ID = task.TaskID}); - } - - private void Compact(IBackup backup, RequestInfo info) - { - var task = Runner.CreateTask(DuplicatiOperation.Compact, backup); - Program.WorkThread.AddTask(task); - Program.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(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(); - 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); - Program.WorkThread.AddTask(task); - Program.StatusEventNotifyer.SignalNewEvent(); - - info.OutputOK(new {Status = "OK", ID = task.TaskID}); - } - - private void RunBackup(IBackup backup, RequestInfo info) - { - var t = Program.WorkThread.CurrentTask; - var bt = t == null ? null : t.Backup; - if (bt != null && backup.ID == bt.ID) - { - // Already running - } - else if (Program.WorkThread.CurrentTasks.Any(x => { - var bn = x?.Backup; - return bn == null || bn.ID == backup.ID; - })) - { - // Already in queue - } - else - { - Program.WorkThread.AddTask(Runner.CreateTask(DuplicatiOperation.Backup, backup), true); - Program.StatusEventNotifyer.SignalNewEvent(); - } - - info.OutputOK(); - } - - private void IsActive(IBackup backup, RequestInfo info) - { - var t = Program.WorkThread.CurrentTask; - var bt = t?.Backup; - if (bt != null && backup.ID == bt.ID) - { - info.OutputOK(new { Status = "OK", Active = true }); - return; - } - else if (Program.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); - - Program.DataConnection.UpdateBackupDBPath(backup, np); - } - - } - - } - - public void GET(string key, RequestInfo info) - { - var parts = (key ?? "").Split(new char[] { '/' }, 2); - var bk = Program.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 = Program.DataConnection.GetScheduleIDsFromTags(new string[] { "ID=" + bk.ID }); - var schedule = scheduleId.Any() ? Program.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 = Program.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(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 = Program.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)) - str = new StreamReader(info.Request.Body, System.Text.Encoding.UTF8).ReadToEnd(); - - if (string.IsNullOrWhiteSpace(str)) - { - info.ReportClientError("Missing backup object", System.Net.HttpStatusCode.BadRequest); - return; - } - - Backups.AddOrUpdateBackupData data = null; - try - { - data = Serializer.Deserialize(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 = Program.DataConnection.GetBackup(data.Backup.ID); - if (backup.IsTemporary) - throw new InvalidDataException("External is temporary but internal is not?"); - - Program.DataConnection.UpdateTemporaryBackup(backup); - info.OutputOK(); - } - else - { - lock(Program.DataConnection.m_lock) - { - var backup = Program.DataConnection.GetBackup(data.Backup.ID); - if (backup == null) - { - info.ReportClientError("Invalid or missing backup id", System.Net.HttpStatusCode.NotFound); - return; - } - - if (Program.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 = Program.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 - Program.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 = Program.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 (Program.WorkThread.Active) - { - try - { - //TODO: It's not safe to access the values like this, - //because the runner thread might interfere - var nt = Program.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 = Program.LiveControl.State == LiveControls.LiveControlState.Paused; - Program.LiveControl.Pause(); - - for(int i = 0; i < 10; i++) - if (Program.WorkThread.Active) - { - var t = Program.WorkThread.CurrentTask; - if (backup.Equals(t == null ? null : t.Backup)) - System.Threading.Thread.Sleep(1000); - else - break; - } - else - break; - - if (Program.WorkThread.Active) - { - var t = Program.WorkThread.CurrentTask; - if (backup.Equals(t == null ? null : t.Backup)) - { - if (hasPaused) - Program.LiveControl.Resume(); - info.OutputError(new { status = "failed", reason = "backup-unstoppable" }); - return; - } - } - - if (hasPaused) - Program.LiveControl.Resume(); - } - } - catch (Exception ex) - { - info.OutputError(new { status = "error", message = ex.Message }); - return; - } - } - - var extra = new Dictionary(); - 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); - Program.WorkThread.AddTask(task); - Program.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> Types - { - get - { - return new KeyValuePair[] { - new KeyValuePair(HttpServer.Method.Get, typeof(GetResponse)), - new KeyValuePair(HttpServer.Method.Put, typeof(Backups.AddOrUpdateBackupData)), - new KeyValuePair(HttpServer.Method.Delete, typeof(long)) - }; - } - } - } -} - +// Copyright (C) 2015, The Duplicati Team +// http://www.duplicati.com, info@duplicati.com +// +// This library is free software; you can redistribute it and/or modify +// it under the terms of the GNU Lesser General Public License as +// published by the Free Software Foundation; either version 2.1 of the +// License, or (at your option) any later version. +// +// This library is distributed in the hope that it will be useful, but +// WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +// Lesser General Public License for more details. +// +// You should have received a copy of the GNU Lesser General Public +// License along with this library; if not, write to the Free Software +// Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA +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 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(); + + 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 + { + ["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())) + { + 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(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(); + 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(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)) + str = new StreamReader(info.Request.Body, System.Text.Encoding.UTF8).ReadToEnd(); + + if (string.IsNullOrWhiteSpace(str)) + { + info.ReportClientError("Missing backup object", System.Net.HttpStatusCode.BadRequest); + return; + } + + Backups.AddOrUpdateBackupData data = null; + try + { + data = Serializer.Deserialize(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(); + 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> Types + { + get + { + return new KeyValuePair[] { + new KeyValuePair(HttpServer.Method.Get, typeof(GetResponse)), + new KeyValuePair(HttpServer.Method.Put, typeof(Backups.AddOrUpdateBackupData)), + new KeyValuePair(HttpServer.Method.Delete, typeof(long)) + }; + } + } + } +} + diff --git a/Duplicati/Server/WebServer/RESTMethods/BackupDefaults.cs b/Duplicati.Library.RestAPI/RESTMethods/BackupDefaults.cs similarity index 96% rename from Duplicati/Server/WebServer/RESTMethods/BackupDefaults.cs rename to Duplicati.Library.RestAPI/RESTMethods/BackupDefaults.cs index 521222397..99d7f22b7 100644 --- a/Duplicati/Server/WebServer/RESTMethods/BackupDefaults.cs +++ b/Duplicati.Library.RestAPI/RESTMethods/BackupDefaults.cs @@ -1,136 +1,137 @@ -// Copyright (C) 2015, The Duplicati Team -// http://www.duplicati.com, info@duplicati.com -// -// This library is free software; you can redistribute it and/or modify -// it under the terms of the GNU Lesser General Public License as -// published by the Free Software Foundation; either version 2.1 of the -// License, or (at your option) any later version. -// -// This library is distributed in the hope that it will be useful, but -// WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU -// Lesser General Public License for more details. -// -// You should have received a copy of the GNU Lesser General Public -// License along with this library; if not, write to the Free Software -// Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA -using System; -using System.Linq; -using Duplicati.Library.Common.IO; -using Duplicati.Library.Utility; - -namespace Duplicati.Server.WebServer.RESTMethods -{ - public class BackupDefaults : IRESTMethodGET - { - - private static readonly string LOGTAG = Library.Logging.Log.LogTagFromType(); - - public void GET(string key, RequestInfo info) - { - // Start with a scratch object - var o = new Newtonsoft.Json.Linq.JObject(); - - // Add application wide settings - o.Add("ApplicationOptions", new Newtonsoft.Json.Linq.JArray( - from n in Program.DataConnection.Settings - select Newtonsoft.Json.Linq.JObject.FromObject(n) - )); - - try - { - // Add built-in defaults - Newtonsoft.Json.Linq.JObject n; - using(var s = new System.IO.StreamReader(System.Reflection.Assembly.GetExecutingAssembly().GetManifestResourceStream(typeof(Program), "newbackup.json"))) - n = (Newtonsoft.Json.Linq.JObject)Newtonsoft.Json.JsonConvert.DeserializeObject(s.ReadToEnd()); - - MergeJsonObjects(o, n); - } - catch (Exception e) - { - Library.Logging.Log.WriteErrorMessage(LOGTAG, "BackupDefaultsError", e, "Failed to locate embeded backup defaults"); - } - - try - { - // Add install defaults/overrides, if present - var path = SystemIO.IO_OS.PathCombine(Library.AutoUpdater.UpdaterManager.InstalledBaseDir, "newbackup.json"); - if (System.IO.File.Exists(path)) - { - Newtonsoft.Json.Linq.JObject n; - n = (Newtonsoft.Json.Linq.JObject)Newtonsoft.Json.JsonConvert.DeserializeObject(System.IO.File.ReadAllText(path)); - - MergeJsonObjects(o, n); - } - } - catch (Exception e) - { - Library.Logging.Log.WriteErrorMessage(LOGTAG, "BackupDefaultsError", e, "Failed to process newbackup.json"); - } - - info.OutputOK(new - { - success = true, - data = o - }); - } - - private static void MergeJsonObjects(Newtonsoft.Json.Linq.JObject self, Newtonsoft.Json.Linq.JObject other) - { - foreach(var p in other.Properties()) - { - var sp = self.Property(p.Name); - if (sp == null) - self.Add(p); - else - { - switch (p.Type) - { - // Primitives override - case Newtonsoft.Json.Linq.JTokenType.Boolean: - case Newtonsoft.Json.Linq.JTokenType.Bytes: - case Newtonsoft.Json.Linq.JTokenType.Comment: - case Newtonsoft.Json.Linq.JTokenType.Constructor: - case Newtonsoft.Json.Linq.JTokenType.Date: - case Newtonsoft.Json.Linq.JTokenType.Float: - case Newtonsoft.Json.Linq.JTokenType.Guid: - case Newtonsoft.Json.Linq.JTokenType.Integer: - case Newtonsoft.Json.Linq.JTokenType.String: - case Newtonsoft.Json.Linq.JTokenType.TimeSpan: - case Newtonsoft.Json.Linq.JTokenType.Uri: - case Newtonsoft.Json.Linq.JTokenType.None: - case Newtonsoft.Json.Linq.JTokenType.Null: - case Newtonsoft.Json.Linq.JTokenType.Undefined: - self.Replace(p); - break; - - // Arrays merge - case Newtonsoft.Json.Linq.JTokenType.Array: - if (sp.Type == Newtonsoft.Json.Linq.JTokenType.Array) - sp.Value = new Newtonsoft.Json.Linq.JArray(((Newtonsoft.Json.Linq.JArray)sp.Value).Union((Newtonsoft.Json.Linq.JArray)p.Value)); - else - { - var a = new Newtonsoft.Json.Linq.JArray(sp.Value); - sp.Value = new Newtonsoft.Json.Linq.JArray(a.Union((Newtonsoft.Json.Linq.JArray)p.Value)); - } - - break; - - // Objects merge - case Newtonsoft.Json.Linq.JTokenType.Object: - if (sp.Type == Newtonsoft.Json.Linq.JTokenType.Object) - MergeJsonObjects((Newtonsoft.Json.Linq.JObject)sp.Value, (Newtonsoft.Json.Linq.JObject)p.Value); - else - sp.Value = p.Value; - break; - - // Ignore other stuff - default: - break; - } - } - } - } - } -} - +// Copyright (C) 2015, The Duplicati Team +// http://www.duplicati.com, info@duplicati.com +// +// This library is free software; you can redistribute it and/or modify +// it under the terms of the GNU Lesser General Public License as +// published by the Free Software Foundation; either version 2.1 of the +// License, or (at your option) any later version. +// +// This library is distributed in the hope that it will be useful, but +// WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +// Lesser General Public License for more details. +// +// You should have received a copy of the GNU Lesser General Public +// License along with this library; if not, write to the Free Software +// Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA +using System; +using System.Linq; +using Duplicati.Library.Common.IO; +using Duplicati.Library.RestAPI; +using Duplicati.Library.Utility; + +namespace Duplicati.Server.WebServer.RESTMethods +{ + public class BackupDefaults : IRESTMethodGET + { + + private static readonly string LOGTAG = Library.Logging.Log.LogTagFromType(); + + public void GET(string key, RequestInfo info) + { + // Start with a scratch object + var o = new Newtonsoft.Json.Linq.JObject(); + + // Add application wide settings + o.Add("ApplicationOptions", new Newtonsoft.Json.Linq.JArray( + from n in FIXMEGlobal.DataConnection.Settings + select Newtonsoft.Json.Linq.JObject.FromObject(n) + )); + + try + { + // Add built-in defaults + Newtonsoft.Json.Linq.JObject n; + using(var s = new System.IO.StreamReader(System.Reflection.Assembly.GetExecutingAssembly().GetManifestResourceStream(typeof(FIXMEGlobal), "newbackup.json"))) + n = (Newtonsoft.Json.Linq.JObject)Newtonsoft.Json.JsonConvert.DeserializeObject(s.ReadToEnd()); + + MergeJsonObjects(o, n); + } + catch (Exception e) + { + Library.Logging.Log.WriteErrorMessage(LOGTAG, "BackupDefaultsError", e, "Failed to locate embeded backup defaults"); + } + + try + { + // Add install defaults/overrides, if present + var path = SystemIO.IO_OS.PathCombine(Library.AutoUpdater.UpdaterManager.InstalledBaseDir, "newbackup.json"); + if (System.IO.File.Exists(path)) + { + Newtonsoft.Json.Linq.JObject n; + n = (Newtonsoft.Json.Linq.JObject)Newtonsoft.Json.JsonConvert.DeserializeObject(System.IO.File.ReadAllText(path)); + + MergeJsonObjects(o, n); + } + } + catch (Exception e) + { + Library.Logging.Log.WriteErrorMessage(LOGTAG, "BackupDefaultsError", e, "Failed to process newbackup.json"); + } + + info.OutputOK(new + { + success = true, + data = o + }); + } + + private static void MergeJsonObjects(Newtonsoft.Json.Linq.JObject self, Newtonsoft.Json.Linq.JObject other) + { + foreach(var p in other.Properties()) + { + var sp = self.Property(p.Name); + if (sp == null) + self.Add(p); + else + { + switch (p.Type) + { + // Primitives override + case Newtonsoft.Json.Linq.JTokenType.Boolean: + case Newtonsoft.Json.Linq.JTokenType.Bytes: + case Newtonsoft.Json.Linq.JTokenType.Comment: + case Newtonsoft.Json.Linq.JTokenType.Constructor: + case Newtonsoft.Json.Linq.JTokenType.Date: + case Newtonsoft.Json.Linq.JTokenType.Float: + case Newtonsoft.Json.Linq.JTokenType.Guid: + case Newtonsoft.Json.Linq.JTokenType.Integer: + case Newtonsoft.Json.Linq.JTokenType.String: + case Newtonsoft.Json.Linq.JTokenType.TimeSpan: + case Newtonsoft.Json.Linq.JTokenType.Uri: + case Newtonsoft.Json.Linq.JTokenType.None: + case Newtonsoft.Json.Linq.JTokenType.Null: + case Newtonsoft.Json.Linq.JTokenType.Undefined: + self.Replace(p); + break; + + // Arrays merge + case Newtonsoft.Json.Linq.JTokenType.Array: + if (sp.Type == Newtonsoft.Json.Linq.JTokenType.Array) + sp.Value = new Newtonsoft.Json.Linq.JArray(((Newtonsoft.Json.Linq.JArray)sp.Value).Union((Newtonsoft.Json.Linq.JArray)p.Value)); + else + { + var a = new Newtonsoft.Json.Linq.JArray(sp.Value); + sp.Value = new Newtonsoft.Json.Linq.JArray(a.Union((Newtonsoft.Json.Linq.JArray)p.Value)); + } + + break; + + // Objects merge + case Newtonsoft.Json.Linq.JTokenType.Object: + if (sp.Type == Newtonsoft.Json.Linq.JTokenType.Object) + MergeJsonObjects((Newtonsoft.Json.Linq.JObject)sp.Value, (Newtonsoft.Json.Linq.JObject)p.Value); + else + sp.Value = p.Value; + break; + + // Ignore other stuff + default: + break; + } + } + } + } + } +} + diff --git a/Duplicati/Server/WebServer/RESTMethods/Backups.cs b/Duplicati.Library.RestAPI/RESTMethods/Backups.cs similarity index 87% rename from Duplicati/Server/WebServer/RESTMethods/Backups.cs rename to Duplicati.Library.RestAPI/RESTMethods/Backups.cs index 91c028013..3bcbfcce1 100644 --- a/Duplicati/Server/WebServer/RESTMethods/Backups.cs +++ b/Duplicati.Library.RestAPI/RESTMethods/Backups.cs @@ -1,299 +1,300 @@ -// Copyright (C) 2015, The Duplicati Team -// http://www.duplicati.com, info@duplicati.com -// -// This library is free software; you can redistribute it and/or modify -// it under the terms of the GNU Lesser General Public License as -// published by the Free Software Foundation; either version 2.1 of the -// License, or (at your option) any later version. -// -// This library is distributed in the hope that it will be useful, but -// WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU -// Lesser General Public License for more details. -// -// You should have received a copy of the GNU Lesser General Public -// License along with this library; if not, write to the Free Software -// Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA -using System; -using System.Linq; -using System.Collections.Generic; -using Duplicati.Server.Serialization; -using System.IO; - -namespace Duplicati.Server.WebServer.RESTMethods -{ - public class Backups : IRESTMethodGET, IRESTMethodPOST, IRESTMethodDocumented - { - public class AddOrUpdateBackupData - { - public Boolean IsUnencryptedOrPassphraseStored { get; set;} - public Database.Schedule Schedule { get; set;} - public Database.Backup Backup { get; set;} - } - - public void GET(string key, RequestInfo info) - { - var schedules = Program.DataConnection.Schedules; - var backups = Program.DataConnection.Backups; - - var all = from n in backups - select new AddOrUpdateBackupData { - IsUnencryptedOrPassphraseStored = Program.DataConnection.IsUnencryptedOrPassphraseStored(long.Parse(n.ID)), - Backup = (Database.Backup)n, - Schedule = - (from x in schedules - where x.Tags != null && x.Tags.Contains("ID=" + n.ID) - select (Database.Schedule)x).FirstOrDefault() - }; - - info.BodyWriter.OutputOK(all.ToArray()); - } - - private void ImportBackup(RequestInfo info) - { - var output_template = ""; - //output_template = ""; - try - { - var input = info.Request.Form; - var cmdline = Library.Utility.Utility.ParseBool(input["cmdline"].Value, false); - var import_metadata = Library.Utility.Utility.ParseBool(input["import_metadata"].Value, false); - var direct = Library.Utility.Utility.ParseBool(input["direct"].Value, false); - output_template = output_template.Replace("CBM", input["callback"].Value); - if (cmdline) - { - info.Response.ContentType = "text/html"; - info.BodyWriter.Write(output_template.Replace("MSG", "Import from commandline not yet implemented")); - } - else - { - var file = info.Request.Form.GetFile("config"); - if (file == null) - throw new Exception("No file uploaded"); - - Serializable.ImportExportStructure ipx = Backups.LoadConfiguration(file.Filename, import_metadata, () => input["passphrase"].Value); - if (direct) - { - lock (Program.DataConnection.m_lock) - { - var basename = ipx.Backup.Name; - var c = 0; - while (c++ < 100 && Program.DataConnection.Backups.Any(x => x.Name.Equals(ipx.Backup.Name, StringComparison.OrdinalIgnoreCase))) - ipx.Backup.Name = basename + " (" + c.ToString() + ")"; - - if (Program.DataConnection.Backups.Any(x => x.Name.Equals(ipx.Backup.Name, StringComparison.OrdinalIgnoreCase))) - { - info.BodyWriter.SetOK(); - info.Response.ContentType = "text/html"; - info.BodyWriter.Write(output_template.Replace("MSG", "There already exists a backup with the name: " + basename.Replace("\'", "\\'"))); - } - - var err = Program.DataConnection.ValidateBackup(ipx.Backup, ipx.Schedule); - if (!string.IsNullOrWhiteSpace(err)) - { - info.ReportClientError(err, System.Net.HttpStatusCode.BadRequest); - return; - } - - Program.DataConnection.AddOrUpdateBackupAndSchedule(ipx.Backup, ipx.Schedule); - } - - info.Response.ContentType = "text/html"; - info.BodyWriter.Write(output_template.Replace("MSG", "OK")); - - } - else - { - using (var sw = new StringWriter()) - { - Serializer.SerializeJson(sw, ipx, true); - output_template = output_template.Replace("'JSO'", sw.ToString()); - } - info.BodyWriter.Write(output_template.Replace("MSG", "Import completed, but a browser issue prevents loading the contents. Try using the direct import method instead.")); - } - } - } - catch (Exception ex) - { - Program.DataConnection.LogError("", "Failed to import backup", ex); - info.Response.ContentType = "text/html"; - info.BodyWriter.Write(output_template.Replace("MSG", ex.Message.Replace("\'", "\\'").Replace("\r", "\\r").Replace("\n", "\\n"))); - } - } - - public static Serializable.ImportExportStructure ImportBackup(string configurationFile, bool importMetadata, Func getPassword, Dictionary advancedOptions) - { - // This removes the ID and DBPath from the backup configuration. - Serializable.ImportExportStructure importedStructure = Backups.LoadConfiguration(configurationFile, importMetadata, getPassword); - - // This will create the Duplicati-server.sqlite database file if it doesn't exist. - using (Duplicati.Server.Database.Connection connection = Program.GetDatabaseConnection(advancedOptions)) - { - if (connection.Backups.Any(x => x.Name.Equals(importedStructure.Backup.Name, StringComparison.OrdinalIgnoreCase))) - { - throw new InvalidOperationException($"A backup with the name {importedStructure.Backup.Name} already exists."); - } - - string error = connection.ValidateBackup(importedStructure.Backup, importedStructure.Schedule); - if (!string.IsNullOrWhiteSpace(error)) - { - throw new InvalidOperationException(error); - } - - // This creates a new ID and DBPath. - connection.AddOrUpdateBackupAndSchedule(importedStructure.Backup, importedStructure.Schedule); - } - - return importedStructure; - } - - private static Serializable.ImportExportStructure LoadConfiguration(string filename, bool importMetadata, Func getPassword) - { - Serializable.ImportExportStructure ipx; - - var buf = new byte[3]; - using (var fs = System.IO.File.OpenRead(filename)) - { - Duplicati.Library.Utility.Utility.ForceStreamRead(fs, buf, buf.Length); - - fs.Position = 0; - if (buf[0] == 'A' && buf[1] == 'E' && buf[2] == 'S') - { - using (var m = new Duplicati.Library.Encryption.AESEncryption(getPassword(), new Dictionary())) - { - using (var m2 = m.Decrypt(fs)) - { - using (var sr = new System.IO.StreamReader(m2)) - { - ipx = Serializer.Deserialize(sr); - } - } - } - } - else - { - using (var sr = new System.IO.StreamReader(fs)) - { - ipx = Serializer.Deserialize(sr); - } - } - } - - if (ipx.Backup == null) - { - throw new Exception("No backup found in document"); - } - - if (ipx.Backup.Metadata == null) - { - ipx.Backup.Metadata = new Dictionary(); - } - - if (!importMetadata) - { - ipx.Backup.Metadata.Clear(); - } - - ipx.Backup.ID = null; - ipx.Backup.DBPath = null; - - if (ipx.Schedule != null) - { - ipx.Schedule.ID = -1; - } - - return ipx; - } - - public void POST(string key, RequestInfo info) - { - if ("import".Equals(key, StringComparison.OrdinalIgnoreCase)) - { - ImportBackup(info); - return; - } - - AddOrUpdateBackupData data = null; - try - { - var str = info.Request.Form["data"].Value; - if (string.IsNullOrWhiteSpace(str)) - str = new StreamReader(info.Request.Body, System.Text.Encoding.UTF8).ReadToEnd(); - - data = Serializer.Deserialize(new StringReader(str)); - if (data.Backup == null) - { - info.ReportClientError("Data object had no backup entry", System.Net.HttpStatusCode.BadRequest); - return; - } - - data.Backup.ID = null; - - if (Duplicati.Library.Utility.Utility.ParseBool(info.Request.Form["temporary"].Value, false)) - { - using(var tf = new Duplicati.Library.Utility.TempFile()) - data.Backup.DBPath = tf; - - data.Backup.Filters = data.Backup.Filters ?? new Duplicati.Server.Serialization.Interface.IFilter[0]; - data.Backup.Settings = data.Backup.Settings ?? new Duplicati.Server.Serialization.Interface.ISetting[0]; - - Program.DataConnection.RegisterTemporaryBackup(data.Backup); - - info.OutputOK(new { status = "OK", ID = data.Backup.ID }); - } - else - { - if (Library.Utility.Utility.ParseBool(info.Request.Form["existing_db"].Value, false)) - { - data.Backup.DBPath = Library.Main.DatabaseLocator.GetDatabasePath(data.Backup.TargetURL, null, false, false); - if (string.IsNullOrWhiteSpace(data.Backup.DBPath)) - throw new Exception("Unable to find remote db path?"); - } - - - lock(Program.DataConnection.m_lock) - { - if (Program.DataConnection.Backups.Any(x => x.Name.Equals(data.Backup.Name, StringComparison.OrdinalIgnoreCase))) - { - info.ReportClientError("There already exists a backup with the name: " + data.Backup.Name, System.Net.HttpStatusCode.Conflict); - return; - } - - var err = Program.DataConnection.ValidateBackup(data.Backup, data.Schedule); - if (!string.IsNullOrWhiteSpace(err)) - { - info.ReportClientError(err, System.Net.HttpStatusCode.BadRequest); - return; - } - - Program.DataConnection.AddOrUpdateBackupAndSchedule(data.Backup, data.Schedule); - } - - info.OutputOK(new { status = "OK", ID = data.Backup.ID }); - } - } - 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 schedule or backup object: {0}", ex.Message), System.Net.HttpStatusCode.InternalServerError); - } - } - - - public string Description { get { return "Return a list of current backups and their schedules"; } } - - public IEnumerable> Types - { - get - { - return new KeyValuePair[] { - new KeyValuePair(HttpServer.Method.Get, typeof(AddOrUpdateBackupData[])), - new KeyValuePair(HttpServer.Method.Post, typeof(AddOrUpdateBackupData)) - }; - } - } - } -} - +// Copyright (C) 2015, The Duplicati Team +// http://www.duplicati.com, info@duplicati.com +// +// This library is free software; you can redistribute it and/or modify +// it under the terms of the GNU Lesser General Public License as +// published by the Free Software Foundation; either version 2.1 of the +// License, or (at your option) any later version. +// +// This library is distributed in the hope that it will be useful, but +// WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +// Lesser General Public License for more details. +// +// You should have received a copy of the GNU Lesser General Public +// License along with this library; if not, write to the Free Software +// Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA +using System; +using System.Linq; +using System.Collections.Generic; +using Duplicati.Server.Serialization; +using System.IO; +using Duplicati.Library.RestAPI; + +namespace Duplicati.Server.WebServer.RESTMethods +{ + public class Backups : IRESTMethodGET, IRESTMethodPOST, IRESTMethodDocumented + { + public class AddOrUpdateBackupData + { + public Boolean IsUnencryptedOrPassphraseStored { get; set;} + public Database.Schedule Schedule { get; set;} + public Database.Backup Backup { get; set;} + } + + public void GET(string key, RequestInfo info) + { + var schedules = FIXMEGlobal.DataConnection.Schedules; + var backups = FIXMEGlobal.DataConnection.Backups; + + var all = from n in backups + select new AddOrUpdateBackupData { + IsUnencryptedOrPassphraseStored = FIXMEGlobal.DataConnection.IsUnencryptedOrPassphraseStored(long.Parse(n.ID)), + Backup = (Database.Backup)n, + Schedule = + (from x in schedules + where x.Tags != null && x.Tags.Contains("ID=" + n.ID) + select (Database.Schedule)x).FirstOrDefault() + }; + + info.BodyWriter.OutputOK(all.ToArray()); + } + + private void ImportBackup(RequestInfo info) + { + var output_template = ""; + //output_template = ""; + try + { + var input = info.Request.Form; + var cmdline = Library.Utility.Utility.ParseBool(input["cmdline"].Value, false); + var import_metadata = Library.Utility.Utility.ParseBool(input["import_metadata"].Value, false); + var direct = Library.Utility.Utility.ParseBool(input["direct"].Value, false); + output_template = output_template.Replace("CBM", input["callback"].Value); + if (cmdline) + { + info.Response.ContentType = "text/html"; + info.BodyWriter.Write(output_template.Replace("MSG", "Import from commandline not yet implemented")); + } + else + { + var file = info.Request.Form.GetFile("config"); + if (file == null) + throw new Exception("No file uploaded"); + + Serializable.ImportExportStructure ipx = Backups.LoadConfiguration(file.Filename, import_metadata, () => input["passphrase"].Value); + if (direct) + { + lock (FIXMEGlobal.DataConnection.m_lock) + { + var basename = ipx.Backup.Name; + var c = 0; + while (c++ < 100 && FIXMEGlobal.DataConnection.Backups.Any(x => x.Name.Equals(ipx.Backup.Name, StringComparison.OrdinalIgnoreCase))) + ipx.Backup.Name = basename + " (" + c.ToString() + ")"; + + if (FIXMEGlobal.DataConnection.Backups.Any(x => x.Name.Equals(ipx.Backup.Name, StringComparison.OrdinalIgnoreCase))) + { + info.BodyWriter.SetOK(); + info.Response.ContentType = "text/html"; + info.BodyWriter.Write(output_template.Replace("MSG", "There already exists a backup with the name: " + basename.Replace("\'", "\\'"))); + } + + var err = FIXMEGlobal.DataConnection.ValidateBackup(ipx.Backup, ipx.Schedule); + if (!string.IsNullOrWhiteSpace(err)) + { + info.ReportClientError(err, System.Net.HttpStatusCode.BadRequest); + return; + } + + FIXMEGlobal.DataConnection.AddOrUpdateBackupAndSchedule(ipx.Backup, ipx.Schedule); + } + + info.Response.ContentType = "text/html"; + info.BodyWriter.Write(output_template.Replace("MSG", "OK")); + + } + else + { + using (var sw = new StringWriter()) + { + Serializer.SerializeJson(sw, ipx, true); + output_template = output_template.Replace("'JSO'", sw.ToString()); + } + info.BodyWriter.Write(output_template.Replace("MSG", "Import completed, but a browser issue prevents loading the contents. Try using the direct import method instead.")); + } + } + } + catch (Exception ex) + { + FIXMEGlobal.DataConnection.LogError("", "Failed to import backup", ex); + info.Response.ContentType = "text/html"; + info.BodyWriter.Write(output_template.Replace("MSG", ex.Message.Replace("\'", "\\'").Replace("\r", "\\r").Replace("\n", "\\n"))); + } + } + + public static Serializable.ImportExportStructure ImportBackup(string configurationFile, bool importMetadata, Func getPassword, Dictionary advancedOptions) + { + // This removes the ID and DBPath from the backup configuration. + Serializable.ImportExportStructure importedStructure = Backups.LoadConfiguration(configurationFile, importMetadata, getPassword); + + // This will create the Duplicati-server.sqlite database file if it doesn't exist. + using (Duplicati.Server.Database.Connection connection = FIXMEGlobal.GetDatabaseConnection(advancedOptions)) + { + if (connection.Backups.Any(x => x.Name.Equals(importedStructure.Backup.Name, StringComparison.OrdinalIgnoreCase))) + { + throw new InvalidOperationException($"A backup with the name {importedStructure.Backup.Name} already exists."); + } + + string error = connection.ValidateBackup(importedStructure.Backup, importedStructure.Schedule); + if (!string.IsNullOrWhiteSpace(error)) + { + throw new InvalidOperationException(error); + } + + // This creates a new ID and DBPath. + connection.AddOrUpdateBackupAndSchedule(importedStructure.Backup, importedStructure.Schedule); + } + + return importedStructure; + } + + private static Serializable.ImportExportStructure LoadConfiguration(string filename, bool importMetadata, Func getPassword) + { + Serializable.ImportExportStructure ipx; + + var buf = new byte[3]; + using (var fs = System.IO.File.OpenRead(filename)) + { + Duplicati.Library.Utility.Utility.ForceStreamRead(fs, buf, buf.Length); + + fs.Position = 0; + if (buf[0] == 'A' && buf[1] == 'E' && buf[2] == 'S') + { + using (var m = new Duplicati.Library.Encryption.AESEncryption(getPassword(), new Dictionary())) + { + using (var m2 = m.Decrypt(fs)) + { + using (var sr = new System.IO.StreamReader(m2)) + { + ipx = Serializer.Deserialize(sr); + } + } + } + } + else + { + using (var sr = new System.IO.StreamReader(fs)) + { + ipx = Serializer.Deserialize(sr); + } + } + } + + if (ipx.Backup == null) + { + throw new Exception("No backup found in document"); + } + + if (ipx.Backup.Metadata == null) + { + ipx.Backup.Metadata = new Dictionary(); + } + + if (!importMetadata) + { + ipx.Backup.Metadata.Clear(); + } + + ipx.Backup.ID = null; + ipx.Backup.DBPath = null; + + if (ipx.Schedule != null) + { + ipx.Schedule.ID = -1; + } + + return ipx; + } + + public void POST(string key, RequestInfo info) + { + if ("import".Equals(key, StringComparison.OrdinalIgnoreCase)) + { + ImportBackup(info); + return; + } + + AddOrUpdateBackupData data = null; + try + { + var str = info.Request.Form["data"].Value; + if (string.IsNullOrWhiteSpace(str)) + str = new StreamReader(info.Request.Body, System.Text.Encoding.UTF8).ReadToEnd(); + + data = Serializer.Deserialize(new StringReader(str)); + if (data.Backup == null) + { + info.ReportClientError("Data object had no backup entry", System.Net.HttpStatusCode.BadRequest); + return; + } + + data.Backup.ID = null; + + if (Duplicati.Library.Utility.Utility.ParseBool(info.Request.Form["temporary"].Value, false)) + { + using(var tf = new Duplicati.Library.Utility.TempFile()) + data.Backup.DBPath = tf; + + data.Backup.Filters = data.Backup.Filters ?? new Duplicati.Server.Serialization.Interface.IFilter[0]; + data.Backup.Settings = data.Backup.Settings ?? new Duplicati.Server.Serialization.Interface.ISetting[0]; + + FIXMEGlobal.DataConnection.RegisterTemporaryBackup(data.Backup); + + info.OutputOK(new { status = "OK", ID = data.Backup.ID }); + } + else + { + if (Library.Utility.Utility.ParseBool(info.Request.Form["existing_db"].Value, false)) + { + data.Backup.DBPath = Library.Main.DatabaseLocator.GetDatabasePath(data.Backup.TargetURL, null, false, false); + if (string.IsNullOrWhiteSpace(data.Backup.DBPath)) + throw new Exception("Unable to find remote db path?"); + } + + + lock(FIXMEGlobal.DataConnection.m_lock) + { + if (FIXMEGlobal.DataConnection.Backups.Any(x => x.Name.Equals(data.Backup.Name, StringComparison.OrdinalIgnoreCase))) + { + 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; + } + + FIXMEGlobal.DataConnection.AddOrUpdateBackupAndSchedule(data.Backup, data.Schedule); + } + + info.OutputOK(new { status = "OK", ID = data.Backup.ID }); + } + } + 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 schedule or backup object: {0}", ex.Message), System.Net.HttpStatusCode.InternalServerError); + } + } + + + public string Description { get { return "Return a list of current backups and their schedules"; } } + + public IEnumerable> Types + { + get + { + return new KeyValuePair[] { + new KeyValuePair(HttpServer.Method.Get, typeof(AddOrUpdateBackupData[])), + new KeyValuePair(HttpServer.Method.Post, typeof(AddOrUpdateBackupData)) + }; + } + } + } +} + diff --git a/Duplicati/Server/WebServer/RESTMethods/BugReport.cs b/Duplicati.Library.RestAPI/RESTMethods/BugReport.cs similarity index 93% rename from Duplicati/Server/WebServer/RESTMethods/BugReport.cs rename to Duplicati.Library.RestAPI/RESTMethods/BugReport.cs index 61aef7a55..d476d72e9 100644 --- a/Duplicati/Server/WebServer/RESTMethods/BugReport.cs +++ b/Duplicati.Library.RestAPI/RESTMethods/BugReport.cs @@ -1,57 +1,58 @@ -// Copyright (C) 2015, The Duplicati Team -// http://www.duplicati.com, info@duplicati.com -// -// This library is free software; you can redistribute it and/or modify -// it under the terms of the GNU Lesser General Public License as -// published by the Free Software Foundation; either version 2.1 of the -// License, or (at your option) any later version. -// -// This library is distributed in the hope that it will be useful, but -// WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU -// Lesser General Public License for more details. -// -// You should have received a copy of the GNU Lesser General Public -// License along with this library; if not, write to the Free Software -// Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA -using System; -using System.Linq; - -namespace Duplicati.Server.WebServer.RESTMethods -{ - public class BugReport : IRESTMethodGET - { - public void GET(string key, RequestInfo info) - { - long id; - long.TryParse(key, out id); - - var tf = Program.DataConnection.GetTempFiles().FirstOrDefault(x => x.ID == id); - if (tf == null) - { - info.ReportClientError("Invalid or missing bugreport id", System.Net.HttpStatusCode.NotFound); - return; - } - - if (!System.IO.File.Exists(tf.Path)) - { - info.ReportClientError("File is missing", System.Net.HttpStatusCode.NotFound); - return; - } - - var filename = "bugreport.zip"; - using(var fs = System.IO.File.OpenRead(tf.Path)) - { - info.Response.ContentLength = fs.Length; - info.Response.AddHeader("Content-Disposition", string.Format("attachment; filename={0}", filename)); - info.Response.ContentType = "application/octet-stream"; - - info.BodyWriter.SetOK(); - info.Response.SendHeaders(); - fs.CopyTo(info.Response.Body); - info.Response.Send(); - } - } - } -} - +// Copyright (C) 2015, The Duplicati Team +// http://www.duplicati.com, info@duplicati.com +// +// This library is free software; you can redistribute it and/or modify +// it under the terms of the GNU Lesser General Public License as +// published by the Free Software Foundation; either version 2.1 of the +// License, or (at your option) any later version. +// +// This library is distributed in the hope that it will be useful, but +// WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +// Lesser General Public License for more details. +// +// You should have received a copy of the GNU Lesser General Public +// License along with this library; if not, write to the Free Software +// Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA +using Duplicati.Library.RestAPI; +using System; +using System.Linq; + +namespace Duplicati.Server.WebServer.RESTMethods +{ + public class BugReport : IRESTMethodGET + { + public void GET(string key, RequestInfo info) + { + long id; + long.TryParse(key, out id); + + var tf = FIXMEGlobal.DataConnection.GetTempFiles().FirstOrDefault(x => x.ID == id); + if (tf == null) + { + info.ReportClientError("Invalid or missing bugreport id", System.Net.HttpStatusCode.NotFound); + return; + } + + if (!System.IO.File.Exists(tf.Path)) + { + info.ReportClientError("File is missing", System.Net.HttpStatusCode.NotFound); + return; + } + + var filename = "bugreport.zip"; + using(var fs = System.IO.File.OpenRead(tf.Path)) + { + info.Response.ContentLength = fs.Length; + info.Response.AddHeader("Content-Disposition", string.Format("attachment; filename={0}", filename)); + info.Response.ContentType = "application/octet-stream"; + + info.BodyWriter.SetOK(); + info.Response.SendHeaders(); + fs.CopyTo(info.Response.Body); + info.Response.Send(); + } + } + } +} + diff --git a/Duplicati/Server/WebServer/RESTMethods/Captcha.cs b/Duplicati.Library.RestAPI/RESTMethods/Captcha.cs similarity index 97% rename from Duplicati/Server/WebServer/RESTMethods/Captcha.cs rename to Duplicati.Library.RestAPI/RESTMethods/Captcha.cs index 41d7a1ac6..67b708364 100644 --- a/Duplicati/Server/WebServer/RESTMethods/Captcha.cs +++ b/Duplicati.Library.RestAPI/RESTMethods/Captcha.cs @@ -1,168 +1,168 @@ -// Copyright (C) 2016, The Duplicati Team -// http://www.duplicati.com, info@duplicati.com -// -// This library is free software; you can redistribute it and/or modify -// it under the terms of the GNU Lesser General Public License as -// published by the Free Software Foundation; either version 2.1 of the -// License, or (at your option) any later version. -// -// This library is distributed in the hope that it will be useful, but -// WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU -// Lesser General Public License for more details. -// -// You should have received a copy of the GNU Lesser General Public -// License along with this library; if not, write to the Free Software -// Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA -using System; -using System.Linq; -using System.Collections.Generic; - -namespace Duplicati.Server.WebServer.RESTMethods -{ - public class Captcha : IRESTMethodGET, IRESTMethodPOST - { - private class CaptchaEntry - { - public readonly string Answer; - public readonly string Target; - public int Attempts; - public readonly DateTime Expires; - - public CaptchaEntry(string answer, string target) - { - Answer = answer; - Target = target; - Attempts = 4; - Expires = DateTime.Now.AddMinutes(2); - } - } - - private static readonly object m_lock = new object(); - private static readonly Dictionary m_captchas = new Dictionary(); - - public static bool SolvedCaptcha(string token, string target, string answer) - { - lock(m_lock) - { - CaptchaEntry tp; - m_captchas.TryGetValue(token ?? string.Empty, out tp); - if (tp == null) - return false; - - if (tp.Attempts > 0) - tp.Attempts--; - - return tp.Attempts >= 0 && string.Equals(tp.Answer, answer, StringComparison.OrdinalIgnoreCase) && tp.Target == target && tp.Expires >= DateTime.Now; - } - } - - public void GET(string key, RequestInfo info) - { - if (string.IsNullOrWhiteSpace(key)) - { - info.ReportClientError("Missing token value", System.Net.HttpStatusCode.Unauthorized); - return; - } - else - { - string answer = null; - lock (m_lock) - { - CaptchaEntry tp; - m_captchas.TryGetValue(key, out tp); - if (tp != null && tp.Expires > DateTime.Now) - answer = tp.Answer; - } - - if (string.IsNullOrWhiteSpace(answer)) - { - info.ReportClientError("No such entry", System.Net.HttpStatusCode.NotFound); - return; - } - - using (var bmp = CaptchaUtil.CreateCaptcha(answer)) - using (var ms = new System.IO.MemoryStream()) - { - info.Response.ContentType = "image/jpeg"; - info.Response.ContentLength = ms.Length; - bmp.Save(ms, System.Drawing.Imaging.ImageFormat.Jpeg); - ms.Position = 0; - - info.Response.ContentType = "image/jpeg"; - info.Response.ContentLength = ms.Length; - info.Response.SendHeaders(); - ms.CopyTo(info.Response.Body); - info.Response.Send(); - } - } - } - - public void POST(string key, RequestInfo info) - { - if (string.IsNullOrWhiteSpace(key)) - { - var target = info.Request.Param["target"].Value; - if (string.IsNullOrWhiteSpace(target)) - { - info.ReportClientError("Missing target parameter", System.Net.HttpStatusCode.BadRequest); - return; - } - - var answer = CaptchaUtil.CreateRandomAnswer(minlength: 6, maxlength: 6); - var nonce = Guid.NewGuid().ToString(); - - string token; - using (var ms = new System.IO.MemoryStream()) - { - var bytes = System.Text.Encoding.UTF8.GetBytes(answer + nonce); - ms.Write(bytes, 0, bytes.Length); - ms.Position = 0; - using(var hasher = Library.Utility.HashFactory.CreateHasher(Library.Utility.HashFactory.SHA256)){ - token = Library.Utility.Utility.Base64PlainToBase64Url(Convert.ToBase64String(hasher.ComputeHash(ms))); - } - } - - lock (m_lock) - { - var expired = m_captchas.Where(x => x.Value.Expires < DateTime.Now).Select(x => x.Key).ToArray(); - foreach (var x in expired) - m_captchas.Remove(x); - - if (m_captchas.Count > 3) - { - info.ReportClientError("Too many captchas, wait 2 minutes and try again", System.Net.HttpStatusCode.ServiceUnavailable); - return; - } - - m_captchas[token] = new CaptchaEntry(answer, target); - } - - info.OutputOK(new - { - token = token - }); - } - else - { - var answer = info.Request.Param["answer"].Value; - var target = info.Request.Param["target"].Value; - if (string.IsNullOrWhiteSpace(answer)) - { - info.ReportClientError("Missing answer parameter", System.Net.HttpStatusCode.BadRequest); - return; - } - if (string.IsNullOrWhiteSpace(target)) - { - info.ReportClientError("Missing target parameter", System.Net.HttpStatusCode.BadRequest); - return; - } - - if (SolvedCaptcha(key, target, answer)) - info.OutputOK(); - else - info.ReportClientError("Incorrect", System.Net.HttpStatusCode.Forbidden); - } - } - } -} +// Copyright (C) 2016, The Duplicati Team +// http://www.duplicati.com, info@duplicati.com +// +// This library is free software; you can redistribute it and/or modify +// it under the terms of the GNU Lesser General Public License as +// published by the Free Software Foundation; either version 2.1 of the +// License, or (at your option) any later version. +// +// This library is distributed in the hope that it will be useful, but +// WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +// Lesser General Public License for more details. +// +// You should have received a copy of the GNU Lesser General Public +// License along with this library; if not, write to the Free Software +// Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA +using System; +using System.Linq; +using System.Collections.Generic; + +namespace Duplicati.Server.WebServer.RESTMethods +{ + public class Captcha : IRESTMethodGET, IRESTMethodPOST + { + private class CaptchaEntry + { + public readonly string Answer; + public readonly string Target; + public int Attempts; + public readonly DateTime Expires; + + public CaptchaEntry(string answer, string target) + { + Answer = answer; + Target = target; + Attempts = 4; + Expires = DateTime.Now.AddMinutes(2); + } + } + + private static readonly object m_lock = new object(); + private static readonly Dictionary m_captchas = new Dictionary(); + + public static bool SolvedCaptcha(string token, string target, string answer) + { + lock(m_lock) + { + CaptchaEntry tp; + m_captchas.TryGetValue(token ?? string.Empty, out tp); + if (tp == null) + return false; + + if (tp.Attempts > 0) + tp.Attempts--; + + return tp.Attempts >= 0 && string.Equals(tp.Answer, answer, StringComparison.OrdinalIgnoreCase) && tp.Target == target && tp.Expires >= DateTime.Now; + } + } + + public void GET(string key, RequestInfo info) + { + if (string.IsNullOrWhiteSpace(key)) + { + info.ReportClientError("Missing token value", System.Net.HttpStatusCode.Unauthorized); + return; + } + else + { + string answer = null; + lock (m_lock) + { + CaptchaEntry tp; + m_captchas.TryGetValue(key, out tp); + if (tp != null && tp.Expires > DateTime.Now) + answer = tp.Answer; + } + + if (string.IsNullOrWhiteSpace(answer)) + { + info.ReportClientError("No such entry", System.Net.HttpStatusCode.NotFound); + return; + } + + using (var bmp = CaptchaUtil.CreateCaptcha(answer)) + using (var ms = new System.IO.MemoryStream()) + { + info.Response.ContentType = "image/jpeg"; + info.Response.ContentLength = ms.Length; + bmp.Save(ms, System.Drawing.Imaging.ImageFormat.Jpeg); + ms.Position = 0; + + info.Response.ContentType = "image/jpeg"; + info.Response.ContentLength = ms.Length; + info.Response.SendHeaders(); + ms.CopyTo(info.Response.Body); + info.Response.Send(); + } + } + } + + public void POST(string key, RequestInfo info) + { + if (string.IsNullOrWhiteSpace(key)) + { + var target = info.Request.Param["target"].Value; + if (string.IsNullOrWhiteSpace(target)) + { + info.ReportClientError("Missing target parameter", System.Net.HttpStatusCode.BadRequest); + return; + } + + var answer = CaptchaUtil.CreateRandomAnswer(minlength: 6, maxlength: 6); + var nonce = Guid.NewGuid().ToString(); + + string token; + using (var ms = new System.IO.MemoryStream()) + { + var bytes = System.Text.Encoding.UTF8.GetBytes(answer + nonce); + ms.Write(bytes, 0, bytes.Length); + ms.Position = 0; + using(var hasher = Library.Utility.HashFactory.CreateHasher(Library.Utility.HashFactory.SHA256)){ + token = Library.Utility.Utility.Base64PlainToBase64Url(Convert.ToBase64String(hasher.ComputeHash(ms))); + } + } + + lock (m_lock) + { + var expired = m_captchas.Where(x => x.Value.Expires < DateTime.Now).Select(x => x.Key).ToArray(); + foreach (var x in expired) + m_captchas.Remove(x); + + if (m_captchas.Count > 3) + { + info.ReportClientError("Too many captchas, wait 2 minutes and try again", System.Net.HttpStatusCode.ServiceUnavailable); + return; + } + + m_captchas[token] = new CaptchaEntry(answer, target); + } + + info.OutputOK(new + { + token = token + }); + } + else + { + var answer = info.Request.Param["answer"].Value; + var target = info.Request.Param["target"].Value; + if (string.IsNullOrWhiteSpace(answer)) + { + info.ReportClientError("Missing answer parameter", System.Net.HttpStatusCode.BadRequest); + return; + } + if (string.IsNullOrWhiteSpace(target)) + { + info.ReportClientError("Missing target parameter", System.Net.HttpStatusCode.BadRequest); + return; + } + + if (SolvedCaptcha(key, target, answer)) + info.OutputOK(); + else + info.ReportClientError("Incorrect", System.Net.HttpStatusCode.Forbidden); + } + } + } +} diff --git a/Duplicati/Server/WebServer/RESTMethods/Changelog.cs b/Duplicati.Library.RestAPI/RESTMethods/Changelog.cs similarity index 94% rename from Duplicati/Server/WebServer/RESTMethods/Changelog.cs rename to Duplicati.Library.RestAPI/RESTMethods/Changelog.cs index 6adb73f0c..202870ee9 100644 --- a/Duplicati/Server/WebServer/RESTMethods/Changelog.cs +++ b/Duplicati.Library.RestAPI/RESTMethods/Changelog.cs @@ -1,74 +1,75 @@ -// Copyright (C) 2015, The Duplicati Team -// http://www.duplicati.com, info@duplicati.com -// -// This library is free software; you can redistribute it and/or modify -// it under the terms of the GNU Lesser General Public License as -// published by the Free Software Foundation; either version 2.1 of the -// License, or (at your option) any later version. -// -// This library is distributed in the hope that it will be useful, but -// WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU -// Lesser General Public License for more details. -// -// You should have received a copy of the GNU Lesser General Public -// License along with this library; if not, write to the Free Software -// Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA -using System; -using System.Collections.Generic; - -namespace Duplicati.Server.WebServer.RESTMethods -{ - public class Changelog : IRESTMethodGET, IRESTMethodDocumented - { - private class GetResponse - { - public string Status; - public string Version; - public string Changelog; - } - - public void GET(string key, RequestInfo info) - { - var fromUpdate = info.Request.QueryString["from-update"].Value; - if (!Library.Utility.Utility.ParseBool(fromUpdate, false)) - { - var path = System.IO.Path.Combine(System.IO.Path.GetDirectoryName(System.Reflection.Assembly.GetExecutingAssembly().Location), "changelog.txt"); - info.OutputOK(new GetResponse() { - Status = "OK", - Version = System.Reflection.Assembly.GetExecutingAssembly().GetName().Version.ToString(), - Changelog = System.IO.File.ReadAllText(path) - }); - } - else - { - var updateInfo = Program.DataConnection.ApplicationSettings.UpdatedVersion; - if (updateInfo == null) - { - info.ReportClientError("No update found", System.Net.HttpStatusCode.NotFound); - } - else - { - info.OutputOK(new GetResponse() { - Status = "OK", - Version = updateInfo.Version, - Changelog = updateInfo.ChangeInfo - }); - } - } - } - - public string Description { get { return "Gets the current changelog"; } } - - public IEnumerable> Types - { - get - { - return new KeyValuePair[] { - new KeyValuePair(HttpServer.Method.Get, typeof(GetResponse)), - }; - } - } - } -} - +// Copyright (C) 2015, The Duplicati Team +// http://www.duplicati.com, info@duplicati.com +// +// This library is free software; you can redistribute it and/or modify +// it under the terms of the GNU Lesser General Public License as +// published by the Free Software Foundation; either version 2.1 of the +// License, or (at your option) any later version. +// +// This library is distributed in the hope that it will be useful, but +// WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +// Lesser General Public License for more details. +// +// You should have received a copy of the GNU Lesser General Public +// License along with this library; if not, write to the Free Software +// Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA +using Duplicati.Library.RestAPI; +using System; +using System.Collections.Generic; + +namespace Duplicati.Server.WebServer.RESTMethods +{ + public class Changelog : IRESTMethodGET, IRESTMethodDocumented + { + private class GetResponse + { + public string Status; + public string Version; + public string Changelog; + } + + public void GET(string key, RequestInfo info) + { + var fromUpdate = info.Request.QueryString["from-update"].Value; + if (!Library.Utility.Utility.ParseBool(fromUpdate, false)) + { + var path = System.IO.Path.Combine(System.IO.Path.GetDirectoryName(System.Reflection.Assembly.GetExecutingAssembly().Location), "changelog.txt"); + info.OutputOK(new GetResponse() { + Status = "OK", + Version = System.Reflection.Assembly.GetExecutingAssembly().GetName().Version.ToString(), + Changelog = System.IO.File.ReadAllText(path) + }); + } + else + { + var updateInfo = FIXMEGlobal.DataConnection.ApplicationSettings.UpdatedVersion; + if (updateInfo == null) + { + info.ReportClientError("No update found", System.Net.HttpStatusCode.NotFound); + } + else + { + info.OutputOK(new GetResponse() { + Status = "OK", + Version = updateInfo.Version, + Changelog = updateInfo.ChangeInfo + }); + } + } + } + + public string Description { get { return "Gets the current changelog"; } } + + public IEnumerable> Types + { + get + { + return new KeyValuePair[] { + new KeyValuePair(HttpServer.Method.Get, typeof(GetResponse)), + }; + } + } + } +} + diff --git a/Duplicati/Server/WebServer/RESTMethods/CommandLine.cs b/Duplicati.Library.RestAPI/RESTMethods/CommandLine.cs similarity index 92% rename from Duplicati/Server/WebServer/RESTMethods/CommandLine.cs rename to Duplicati.Library.RestAPI/RESTMethods/CommandLine.cs index 00e6bb373..3b2d6a28a 100644 --- a/Duplicati/Server/WebServer/RESTMethods/CommandLine.cs +++ b/Duplicati.Library.RestAPI/RESTMethods/CommandLine.cs @@ -1,283 +1,284 @@ -// Copyright (C) 2017, The Duplicati Team -// http://www.duplicati.com, info@duplicati.com -// -// This library is free software; you can redistribute it and/or modify -// it under the terms of the GNU Lesser General Public License as -// published by the Free Software Foundation; either version 2.1 of the -// License, or (at your option) any later version. -// -// This library is distributed in the hope that it will be useful, but -// WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU -// Lesser General Public License for more details. -// -// You should have received a copy of the GNU Lesser General Public -// License along with this library; if not, write to the Free Software -// Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA -using System; -using System.Collections.Generic; -using System.IO; -using System.Linq; -using System.Text; - -namespace Duplicati.Server.WebServer.RESTMethods -{ - public class CommandLine : IRESTMethodGET, IRESTMethodPOST - { - private static readonly string LOGTAG = Library.Logging.Log.LogTagFromType(); - - private class LogWriter : System.IO.TextWriter - { - private readonly ActiveRun m_target; - private readonly StringBuilder m_sb = new StringBuilder(); - private int m_newlinechars = 0; - - public LogWriter(ActiveRun target) - { - m_target = target; - } - - public override Encoding Encoding { get { return System.Text.Encoding.UTF8; } } - - public override void Write(char value) - { - lock(m_target.Lock) - { - m_sb.Append(value); - if (NewLine[m_newlinechars] == value) - { - m_newlinechars++; - if (m_newlinechars == NewLine.Length) - WriteLine(string.Empty); - } - else - m_newlinechars = 0; - } - } - - public override void WriteLine(string value) - { - value = value ?? string.Empty; - lock(m_target.Lock) - { - m_target.LastAccess = DateTime.Now; - - //Avoid writing the log if it does not exist - if (m_target.IsLogDisposed) - { - Program.LogHandler.WriteMessage(new Library.Logging.LogEntry("Attempted to write message after closing: {0}", new object[] { value }, Library.Logging.LogMessageType.Warning, LOGTAG, "CommandLineOutputAfterLogClosed", null)); - return; - } - - try - { - if (m_sb.Length != 0) - { - m_target.Log.Add(m_sb + value); - m_sb.Length = 0; - m_newlinechars = 0; - } - else - { - m_target.Log.Add(value); - } - } - catch (Exception ex) - { - // This can happen on a very unlucky race where IsLogDisposed is set right after the check - Program.LogHandler.WriteMessage(new Library.Logging.LogEntry("Failed to forward commandline message: {0}", new object[] { value }, Library.Logging.LogMessageType.Warning, LOGTAG, "CommandLineOutputAfterLogClosed", ex)); - } - } - } - } - - private class ActiveRun - { - public readonly string ID = Guid.NewGuid().ToString(); - public DateTime LastAccess = DateTime.Now; - public readonly Library.Utility.FileBackedStringList Log = new Library.Utility.FileBackedStringList(); - public Runner.IRunnerData Task; - public LogWriter Writer; - public readonly object Lock = new object(); - public bool Finished = false; - public bool Started = false; - public bool IsLogDisposed = false; - public System.Threading.Thread Thread; - } - - private readonly Dictionary m_activeItems = new Dictionary(); - private System.Threading.Tasks.Task m_cleanupTask; - - public void POST(string key, RequestInfo info) - { - if (string.IsNullOrWhiteSpace(key)) - { - string[] args; - using (var sr = new StreamReader(info.Request.Body, System.Text.Encoding.UTF8, true)) - args = Newtonsoft.Json.JsonConvert.DeserializeObject(sr.ReadToEnd()); - - var k = new ActiveRun(); - k.Writer = new LogWriter(k); - - m_activeItems[k.ID] = k; - StartCleanupTask(); - - k.Task = Runner.CreateCustomTask((sink) => - { - try - { - k.Thread = System.Threading.Thread.CurrentThread; - k.Started = true; - - var code = Duplicati.CommandLine.Program.RunCommandLine(k.Writer, k.Writer, c => { - k.Task.SetController(c); - c.AppendSink(sink); - }, args); - k.Writer.WriteLine("Return code: {0}", code); - } - catch (Exception ex) - { - var rx = ex; - if (rx is System.Reflection.TargetInvocationException) - rx = rx.InnerException; - - if (rx is Library.Interface.UserInformationException) - k.Log.Add(rx.Message); - else - k.Log.Add(rx.ToString()); - - throw rx; - } - finally - { - k.Finished = true; - k.Thread = null; - } - }); - - Program.WorkThread.AddTask(k.Task); - - info.OutputOK(new - { - ID = k.ID - }); - } - else - { - if (!key.EndsWith("/abort", StringComparison.OrdinalIgnoreCase)) - { - info.ReportClientError("Only abort commands are allowed", System.Net.HttpStatusCode.BadRequest); - return; - } - - key = key.Substring(0, key.Length - "/abort".Length); - if (string.IsNullOrWhiteSpace(key)) - { - info.ReportClientError("No task key found", System.Net.HttpStatusCode.BadRequest); - return; - } - - ActiveRun t; - if (!m_activeItems.TryGetValue(key, out t)) - { - info.OutputError(code: System.Net.HttpStatusCode.NotFound); - return; - } - - var tt = t.Task; - if (tt != null) - tt.Abort(); - - var tr = t.Thread; - if (tr != null) - tr.Interrupt(); - - info.OutputOK(); - } - } - - private void StartCleanupTask() - { - if (m_cleanupTask == null || m_cleanupTask.IsCompleted || m_cleanupTask.IsFaulted || m_cleanupTask.IsCanceled) - m_cleanupTask = RunCleanupAsync(); - } - - private async System.Threading.Tasks.Task RunCleanupAsync() - { - while (m_activeItems.Count > 0) - { - var oldest = m_activeItems.Values - .OrderBy(x => x.LastAccess) - .FirstOrDefault(); - - if (oldest != null) - { - // If the task has finished, we just wait a little to allow the UI to pick it up - var timeout = oldest.Finished ? TimeSpan.FromMinutes(5) : TimeSpan.FromDays(1); - if (DateTime.Now - oldest.LastAccess > timeout) - { - oldest.IsLogDisposed = true; - m_activeItems.Remove(oldest.ID); - oldest.Log.Dispose(); - - // Fix all expired, or stop running - continue; - } - } - - await System.Threading.Tasks.Task.Delay(TimeSpan.FromMinutes(1)).ConfigureAwait(false); - } - } - - public void GET(string key, RequestInfo info) - { - if (string.IsNullOrWhiteSpace(key)) - { - info.OutputOK( - Duplicati.CommandLine.Program.SupportedCommands - ); - } - else - { - ActiveRun t; - if (!m_activeItems.TryGetValue(key, out t)) - { - info.OutputError(code: System.Net.HttpStatusCode.NotFound); - return; - } - - int pagesize; - int offset; - - int.TryParse(info.Request.QueryString["pagesize"].Value, out pagesize); - int.TryParse(info.Request.QueryString["offset"].Value, out offset); - pagesize = Math.Max(10, Math.Min(500, pagesize)); - offset = Math.Max(0, offset); - var items = new List(); - long count; - bool started; - bool finished; - - lock(t.Lock) - { - t.LastAccess = DateTime.Now; - count = t.Log.Count; - offset = Math.Min((int)count, offset); - items.AddRange(t.Log.Skip(offset).Take(pagesize)); - finished = t.Finished; - started = t.Started; - } - - info.OutputOK(new - { - Pagesize = pagesize, - Offset = offset, - Count = count, - Items = items, - Finished = finished, - Started = started - }); - } - } - } -} +// Copyright (C) 2017, The Duplicati Team +// http://www.duplicati.com, info@duplicati.com +// +// This library is free software; you can redistribute it and/or modify +// it under the terms of the GNU Lesser General Public License as +// published by the Free Software Foundation; either version 2.1 of the +// License, or (at your option) any later version. +// +// This library is distributed in the hope that it will be useful, but +// WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +// Lesser General Public License for more details. +// +// You should have received a copy of the GNU Lesser General Public +// License along with this library; if not, write to the Free Software +// Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA +using Duplicati.Library.RestAPI; +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Text; + +namespace Duplicati.Server.WebServer.RESTMethods +{ + public class CommandLine : IRESTMethodGET, IRESTMethodPOST + { + private static readonly string LOGTAG = Library.Logging.Log.LogTagFromType(); + + private class LogWriter : System.IO.TextWriter + { + private readonly ActiveRun m_target; + private readonly StringBuilder m_sb = new StringBuilder(); + private int m_newlinechars = 0; + + public LogWriter(ActiveRun target) + { + m_target = target; + } + + public override Encoding Encoding { get { return System.Text.Encoding.UTF8; } } + + public override void Write(char value) + { + lock(m_target.Lock) + { + m_sb.Append(value); + if (NewLine[m_newlinechars] == value) + { + m_newlinechars++; + if (m_newlinechars == NewLine.Length) + WriteLine(string.Empty); + } + else + m_newlinechars = 0; + } + } + + public override void WriteLine(string value) + { + value = value ?? string.Empty; + lock(m_target.Lock) + { + m_target.LastAccess = DateTime.Now; + + //Avoid writing the log if it does not exist + if (m_target.IsLogDisposed) + { + FIXMEGlobal.LogHandler.WriteMessage(new Library.Logging.LogEntry("Attempted to write message after closing: {0}", new object[] { value }, Library.Logging.LogMessageType.Warning, LOGTAG, "CommandLineOutputAfterLogClosed", null)); + return; + } + + try + { + if (m_sb.Length != 0) + { + m_target.Log.Add(m_sb + value); + m_sb.Length = 0; + m_newlinechars = 0; + } + else + { + m_target.Log.Add(value); + } + } + catch (Exception ex) + { + // This can happen on a very unlucky race where IsLogDisposed is set right after the check + FIXMEGlobal.LogHandler.WriteMessage(new Library.Logging.LogEntry("Failed to forward commandline message: {0}", new object[] { value }, Library.Logging.LogMessageType.Warning, LOGTAG, "CommandLineOutputAfterLogClosed", ex)); + } + } + } + } + + private class ActiveRun + { + public readonly string ID = Guid.NewGuid().ToString(); + public DateTime LastAccess = DateTime.Now; + public readonly Library.Utility.FileBackedStringList Log = new Library.Utility.FileBackedStringList(); + public Runner.IRunnerData Task; + public LogWriter Writer; + public readonly object Lock = new object(); + public bool Finished = false; + public bool Started = false; + public bool IsLogDisposed = false; + public System.Threading.Thread Thread; + } + + private readonly Dictionary m_activeItems = new Dictionary(); + private System.Threading.Tasks.Task m_cleanupTask; + + public void POST(string key, RequestInfo info) + { + if (string.IsNullOrWhiteSpace(key)) + { + string[] args; + using (var sr = new StreamReader(info.Request.Body, System.Text.Encoding.UTF8, true)) + args = Newtonsoft.Json.JsonConvert.DeserializeObject(sr.ReadToEnd()); + + var k = new ActiveRun(); + k.Writer = new LogWriter(k); + + m_activeItems[k.ID] = k; + StartCleanupTask(); + + k.Task = Runner.CreateCustomTask((sink) => + { + try + { + k.Thread = System.Threading.Thread.CurrentThread; + k.Started = true; + + var code = Duplicati.CommandLine.Program.RunCommandLine(k.Writer, k.Writer, c => { + k.Task.SetController(c); + c.AppendSink(sink); + }, args); + k.Writer.WriteLine("Return code: {0}", code); + } + catch (Exception ex) + { + var rx = ex; + if (rx is System.Reflection.TargetInvocationException) + rx = rx.InnerException; + + if (rx is Library.Interface.UserInformationException) + k.Log.Add(rx.Message); + else + k.Log.Add(rx.ToString()); + + throw rx; + } + finally + { + k.Finished = true; + k.Thread = null; + } + }); + + FIXMEGlobal.WorkThread.AddTask(k.Task); + + info.OutputOK(new + { + ID = k.ID + }); + } + else + { + if (!key.EndsWith("/abort", StringComparison.OrdinalIgnoreCase)) + { + info.ReportClientError("Only abort commands are allowed", System.Net.HttpStatusCode.BadRequest); + return; + } + + key = key.Substring(0, key.Length - "/abort".Length); + if (string.IsNullOrWhiteSpace(key)) + { + info.ReportClientError("No task key found", System.Net.HttpStatusCode.BadRequest); + return; + } + + ActiveRun t; + if (!m_activeItems.TryGetValue(key, out t)) + { + info.OutputError(code: System.Net.HttpStatusCode.NotFound); + return; + } + + var tt = t.Task; + if (tt != null) + tt.Abort(); + + var tr = t.Thread; + if (tr != null) + tr.Interrupt(); + + info.OutputOK(); + } + } + + private void StartCleanupTask() + { + if (m_cleanupTask == null || m_cleanupTask.IsCompleted || m_cleanupTask.IsFaulted || m_cleanupTask.IsCanceled) + m_cleanupTask = RunCleanupAsync(); + } + + private async System.Threading.Tasks.Task RunCleanupAsync() + { + while (m_activeItems.Count > 0) + { + var oldest = m_activeItems.Values + .OrderBy(x => x.LastAccess) + .FirstOrDefault(); + + if (oldest != null) + { + // If the task has finished, we just wait a little to allow the UI to pick it up + var timeout = oldest.Finished ? TimeSpan.FromMinutes(5) : TimeSpan.FromDays(1); + if (DateTime.Now - oldest.LastAccess > timeout) + { + oldest.IsLogDisposed = true; + m_activeItems.Remove(oldest.ID); + oldest.Log.Dispose(); + + // Fix all expired, or stop running + continue; + } + } + + await System.Threading.Tasks.Task.Delay(TimeSpan.FromMinutes(1)).ConfigureAwait(false); + } + } + + public void GET(string key, RequestInfo info) + { + if (string.IsNullOrWhiteSpace(key)) + { + info.OutputOK( + Duplicati.CommandLine.Program.SupportedCommands + ); + } + else + { + ActiveRun t; + if (!m_activeItems.TryGetValue(key, out t)) + { + info.OutputError(code: System.Net.HttpStatusCode.NotFound); + return; + } + + int pagesize; + int offset; + + int.TryParse(info.Request.QueryString["pagesize"].Value, out pagesize); + int.TryParse(info.Request.QueryString["offset"].Value, out offset); + pagesize = Math.Max(10, Math.Min(500, pagesize)); + offset = Math.Max(0, offset); + var items = new List(); + long count; + bool started; + bool finished; + + lock(t.Lock) + { + t.LastAccess = DateTime.Now; + count = t.Log.Count; + offset = Math.Min((int)count, offset); + items.AddRange(t.Log.Skip(offset).Take(pagesize)); + finished = t.Finished; + started = t.Started; + } + + info.OutputOK(new + { + Pagesize = pagesize, + Offset = offset, + Count = count, + Items = items, + Finished = finished, + Started = started + }); + } + } + } +} diff --git a/Duplicati/Server/WebServer/RESTMethods/Filesystem.cs b/Duplicati.Library.RestAPI/RESTMethods/Filesystem.cs similarity index 97% rename from Duplicati/Server/WebServer/RESTMethods/Filesystem.cs rename to Duplicati.Library.RestAPI/RESTMethods/Filesystem.cs index 3c2b25aed..a7a59bbde 100644 --- a/Duplicati/Server/WebServer/RESTMethods/Filesystem.cs +++ b/Duplicati.Library.RestAPI/RESTMethods/Filesystem.cs @@ -1,273 +1,273 @@ -// Copyright (C) 2015, The Duplicati Team -// http://www.duplicati.com, info@duplicati.com -// -// This library is free software; you can redistribute it and/or modify -// it under the terms of the GNU Lesser General Public License as -// published by the Free Software Foundation; either version 2.1 of the -// License, or (at your option) any later version. -// -// This library is distributed in the hope that it will be useful, but -// WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU -// Lesser General Public License for more details. -// -// You should have received a copy of the GNU Lesser General Public -// License along with this library; if not, write to the Free Software -// Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA -using System; -using System.Collections.Generic; -using System.Linq; -using System.IO; -using Duplicati.Library.Snapshots; +// Copyright (C) 2015, The Duplicati Team +// http://www.duplicati.com, info@duplicati.com +// +// This library is free software; you can redistribute it and/or modify +// it under the terms of the GNU Lesser General Public License as +// published by the Free Software Foundation; either version 2.1 of the +// License, or (at your option) any later version. +// +// This library is distributed in the hope that it will be useful, but +// WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +// Lesser General Public License for more details. +// +// You should have received a copy of the GNU Lesser General Public +// License along with this library; if not, write to the Free Software +// Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA +using System; +using System.Collections.Generic; +using System.Linq; +using System.IO; +using Duplicati.Library.Snapshots; using Duplicati.Library.Common.IO; using Duplicati.Library.Common; -namespace Duplicati.Server.WebServer.RESTMethods -{ - public class Filesystem : IRESTMethodGET, IRESTMethodPOST, IRESTMethodDocumented - { - public void GET(string key, RequestInfo info) - { - var parts = (key ?? "").Split(new char[] { '/' }); - var path = Duplicati.Library.Utility.Uri.UrlDecode((parts.Length == 2 ? parts.FirstOrDefault() : key ?? "")); - var command = parts.Length == 2 ? parts.Last() : null; - if (string.IsNullOrEmpty(path)) - path = info.Request.QueryString["path"].Value; - - Process(command, path, info); - } - - private void Process(string command, string path, RequestInfo info) - { - if (string.IsNullOrEmpty(path)) - { - info.ReportClientError("No path parameter was found", System.Net.HttpStatusCode.BadRequest); - return; - } - - bool skipFiles = Library.Utility.Utility.ParseBool(info.Request.QueryString["onlyfolders"].Value, false); - bool showHidden = Library.Utility.Utility.ParseBool(info.Request.QueryString["showhidden"].Value, false); - - string specialpath = null; - string specialtoken = null; - - if (path.StartsWith("%", StringComparison.Ordinal)) - { - var ix = path.IndexOf("%", 1, StringComparison.Ordinal); - if (ix > 0) - { - var tk = path.Substring(0, ix + 1); - var node = SpecialFolders.Nodes.FirstOrDefault(x => x.id.Equals(tk, StringComparison.OrdinalIgnoreCase)); - if (node != null) - { - specialpath = node.resolvedpath; - specialtoken = node.id; - } - } - } - - path = SpecialFolders.ExpandEnvironmentVariables(path); - - if (Platform.IsClientPosix && !path.StartsWith("/", StringComparison.Ordinal)) - { - info.ReportClientError("The path parameter must start with a forward-slash", System.Net.HttpStatusCode.BadRequest); - return; - } - - if (!string.IsNullOrWhiteSpace(command)) - { - if ("validate".Equals(command, StringComparison.OrdinalIgnoreCase)) - { - try - { - if (System.IO.Path.IsPathRooted(path) && (System.IO.Directory.Exists(path) || System.IO.File.Exists(path))) - { - info.OutputOK(); - return; - } - } - catch - { - } - - info.ReportServerError("File or folder not found", System.Net.HttpStatusCode.NotFound); - return; - } - else - { - info.ReportClientError(string.Format("No such operation found: {0}", command), System.Net.HttpStatusCode.NotFound); - return; - } - } - - try - { - if (path != "" && path != "/") - path = Util.AppendDirSeparator(path); - - IEnumerable res; - - if (!Platform.IsClientPosix && (path.Equals("/") || path.Equals(""))) - { - res = DriveInfo.GetDrives() - .Where(di => - (di.DriveType == DriveType.Fixed || di.DriveType == DriveType.Network || di.DriveType == DriveType.Removable) - && di.IsReady // Only try to create TreeNode entries for drives who were ready 'now' - ) - .Select(TryCreateTreeNodeForDrive) // This will try to create a TreeNode for selected drives - .Where(tn => tn != null); // This filters out such entries that could not be created - } - else - { - res = ListFolderAsNodes(path, skipFiles, showHidden); - } - - if ((path.Equals("/") || path.Equals("")) && specialtoken == null) - { - // Prepend special folders - res = SpecialFolders.Nodes.Union(res); - } - - if (specialtoken != null) - { - res = res.Select(x => { - x.resolvedpath = x.id; - x.id = specialtoken + x.id.Substring(specialpath.Length); - return x; - }); - } - - // We have to resolve the query before giving it to OutputOK - // If we do not do this, and the query throws an exception when OutputOK resolves it, - // the exception would not be handled properly - res = res.ToList(); - - info.OutputOK(res); - } - catch (Exception ex) - { - info.ReportClientError("Failed to process the path: " + ex.Message, System.Net.HttpStatusCode.InternalServerError); - } - } - - /// - /// Try to create a new TreeNode instance for the given DriveInfo instance. - /// - /// - /// If an exception occurs during creation (most likely the device became unavailable), a null is returned instead. - /// - /// - /// DriveInfo to try create a TreeNode for. Cannot be null. - /// A new TreeNode instance on success; null if an exception occurred during creation. - private static Serializable.TreeNode TryCreateTreeNodeForDrive(DriveInfo driveInfo) - { - if (driveInfo == null) throw new ArgumentNullException(nameof(driveInfo)); - - try - { - // Try to create the TreeNode - // This may still fail as the drive might become unavailable in the meanwhile - return new Serializable.TreeNode - { - id = driveInfo.RootDirectory.FullName, - text = - ( - string.IsNullOrWhiteSpace(driveInfo.VolumeLabel) - ? driveInfo.RootDirectory.FullName.Replace('\\', ' ') - : driveInfo.VolumeLabel + " - " + driveInfo.RootDirectory.FullName.Replace('\\', ' ') - ) + "(" + driveInfo.DriveType + ")", - iconCls = "x-tree-icon-drive" - }; - } - catch - { - // Drive became unavailable in the meanwhile or another exception occurred - // Return a null as fall back - return null; - } - } - - private static IEnumerable ListFolderAsNodes(string entrypath, bool skipFiles, bool showHidden) - { - //Helper function for finding out if a folder has sub elements - Func hasSubElements = (p) => skipFiles ? Directory.EnumerateDirectories(p).Any() : Directory.EnumerateFileSystemEntries(p).Any(); - - //Helper function for dealing with exceptions when accessing off-limits folders - Func isEmptyFolder = (p) => - { - try { return !hasSubElements(p); } - catch { } - return true; - }; - - //Helper function for dealing with exceptions when accessing off-limits folders - Func canAccess = (p) => - { - try { hasSubElements(p); return true; } - catch { } - return false; - }; - - foreach (var s in SystemIO.IO_OS.EnumerateFileSystemEntries(entrypath) - // Group directories first - .OrderByDescending(f => SystemIO.IO_OS.GetFileAttributes(f) & FileAttributes.Directory) - // Sort both groups (directories and files) alphabetically - .ThenBy(f => f)) - { - Serializable.TreeNode tn = null; - try - { - var attr = SystemIO.IO_OS.GetFileAttributes(s); - var isSymlink = SystemIO.IO_OS.IsSymlink(s, attr); - var isFolder = (attr & FileAttributes.Directory) != 0; - var isFile = !isFolder; - var isHidden = (attr & FileAttributes.Hidden) != 0; - - var accessible = isFile || canAccess(s); - var isLeaf = isFile || !accessible || isEmptyFolder(s); - - var rawid = isFolder ? Util.AppendDirSeparator(s) : s; - if (skipFiles && !isFolder) - continue; - - if (!showHidden && isHidden) - continue; - - tn = new Serializable.TreeNode() - { - id = rawid, - text = SystemIO.IO_OS.PathGetFileName(s), - hidden = isHidden, - symlink = isSymlink, - iconCls = isFolder ? (accessible ? (isSymlink ? "x-tree-icon-symlink" : "x-tree-icon-parent") : "x-tree-icon-locked") : "x-tree-icon-leaf", - leaf = isLeaf - }; - } - catch - { - } - - if (tn != null) - yield return tn; - } - } - - public void POST(string key, RequestInfo info) - { - Process(key, info.Request.Form["path"].Value, info); - } - - public string Description { get { return "Enumerates the server filesystem"; } } - - public IEnumerable> Types - { - get - { - return new KeyValuePair[] { - new KeyValuePair(HttpServer.Method.Get, typeof(string[])), - }; - } - } - } -} - +namespace Duplicati.Server.WebServer.RESTMethods +{ + public class Filesystem : IRESTMethodGET, IRESTMethodPOST, IRESTMethodDocumented + { + public void GET(string key, RequestInfo info) + { + var parts = (key ?? "").Split(new char[] { '/' }); + var path = Duplicati.Library.Utility.Uri.UrlDecode((parts.Length == 2 ? parts.FirstOrDefault() : key ?? "")); + var command = parts.Length == 2 ? parts.Last() : null; + if (string.IsNullOrEmpty(path)) + path = info.Request.QueryString["path"].Value; + + Process(command, path, info); + } + + private void Process(string command, string path, RequestInfo info) + { + if (string.IsNullOrEmpty(path)) + { + info.ReportClientError("No path parameter was found", System.Net.HttpStatusCode.BadRequest); + return; + } + + bool skipFiles = Library.Utility.Utility.ParseBool(info.Request.QueryString["onlyfolders"].Value, false); + bool showHidden = Library.Utility.Utility.ParseBool(info.Request.QueryString["showhidden"].Value, false); + + string specialpath = null; + string specialtoken = null; + + if (path.StartsWith("%", StringComparison.Ordinal)) + { + var ix = path.IndexOf("%", 1, StringComparison.Ordinal); + if (ix > 0) + { + var tk = path.Substring(0, ix + 1); + var node = SpecialFolders.Nodes.FirstOrDefault(x => x.id.Equals(tk, StringComparison.OrdinalIgnoreCase)); + if (node != null) + { + specialpath = node.resolvedpath; + specialtoken = node.id; + } + } + } + + path = SpecialFolders.ExpandEnvironmentVariables(path); + + if (Platform.IsClientPosix && !path.StartsWith("/", StringComparison.Ordinal)) + { + info.ReportClientError("The path parameter must start with a forward-slash", System.Net.HttpStatusCode.BadRequest); + return; + } + + if (!string.IsNullOrWhiteSpace(command)) + { + if ("validate".Equals(command, StringComparison.OrdinalIgnoreCase)) + { + try + { + if (System.IO.Path.IsPathRooted(path) && (System.IO.Directory.Exists(path) || System.IO.File.Exists(path))) + { + info.OutputOK(); + return; + } + } + catch + { + } + + info.ReportServerError("File or folder not found", System.Net.HttpStatusCode.NotFound); + return; + } + else + { + info.ReportClientError(string.Format("No such operation found: {0}", command), System.Net.HttpStatusCode.NotFound); + return; + } + } + + try + { + if (path != "" && path != "/") + path = Util.AppendDirSeparator(path); + + IEnumerable res; + + if (!Platform.IsClientPosix && (path.Equals("/") || path.Equals(""))) + { + res = DriveInfo.GetDrives() + .Where(di => + (di.DriveType == DriveType.Fixed || di.DriveType == DriveType.Network || di.DriveType == DriveType.Removable) + && di.IsReady // Only try to create TreeNode entries for drives who were ready 'now' + ) + .Select(TryCreateTreeNodeForDrive) // This will try to create a TreeNode for selected drives + .Where(tn => tn != null); // This filters out such entries that could not be created + } + else + { + res = ListFolderAsNodes(path, skipFiles, showHidden); + } + + if ((path.Equals("/") || path.Equals("")) && specialtoken == null) + { + // Prepend special folders + res = SpecialFolders.Nodes.Union(res); + } + + if (specialtoken != null) + { + res = res.Select(x => { + x.resolvedpath = x.id; + x.id = specialtoken + x.id.Substring(specialpath.Length); + return x; + }); + } + + // We have to resolve the query before giving it to OutputOK + // If we do not do this, and the query throws an exception when OutputOK resolves it, + // the exception would not be handled properly + res = res.ToList(); + + info.OutputOK(res); + } + catch (Exception ex) + { + info.ReportClientError("Failed to process the path: " + ex.Message, System.Net.HttpStatusCode.InternalServerError); + } + } + + /// + /// Try to create a new TreeNode instance for the given DriveInfo instance. + /// + /// + /// If an exception occurs during creation (most likely the device became unavailable), a null is returned instead. + /// + /// + /// DriveInfo to try create a TreeNode for. Cannot be null. + /// A new TreeNode instance on success; null if an exception occurred during creation. + private static Serializable.TreeNode TryCreateTreeNodeForDrive(DriveInfo driveInfo) + { + if (driveInfo == null) throw new ArgumentNullException(nameof(driveInfo)); + + try + { + // Try to create the TreeNode + // This may still fail as the drive might become unavailable in the meanwhile + return new Serializable.TreeNode + { + id = driveInfo.RootDirectory.FullName, + text = + ( + string.IsNullOrWhiteSpace(driveInfo.VolumeLabel) + ? driveInfo.RootDirectory.FullName.Replace('\\', ' ') + : driveInfo.VolumeLabel + " - " + driveInfo.RootDirectory.FullName.Replace('\\', ' ') + ) + "(" + driveInfo.DriveType + ")", + iconCls = "x-tree-icon-drive" + }; + } + catch + { + // Drive became unavailable in the meanwhile or another exception occurred + // Return a null as fall back + return null; + } + } + + private static IEnumerable ListFolderAsNodes(string entrypath, bool skipFiles, bool showHidden) + { + //Helper function for finding out if a folder has sub elements + Func hasSubElements = (p) => skipFiles ? Directory.EnumerateDirectories(p).Any() : Directory.EnumerateFileSystemEntries(p).Any(); + + //Helper function for dealing with exceptions when accessing off-limits folders + Func isEmptyFolder = (p) => + { + try { return !hasSubElements(p); } + catch { } + return true; + }; + + //Helper function for dealing with exceptions when accessing off-limits folders + Func canAccess = (p) => + { + try { hasSubElements(p); return true; } + catch { } + return false; + }; + + foreach (var s in SystemIO.IO_OS.EnumerateFileSystemEntries(entrypath) + // Group directories first + .OrderByDescending(f => SystemIO.IO_OS.GetFileAttributes(f) & FileAttributes.Directory) + // Sort both groups (directories and files) alphabetically + .ThenBy(f => f)) + { + Serializable.TreeNode tn = null; + try + { + var attr = SystemIO.IO_OS.GetFileAttributes(s); + var isSymlink = SystemIO.IO_OS.IsSymlink(s, attr); + var isFolder = (attr & FileAttributes.Directory) != 0; + var isFile = !isFolder; + var isHidden = (attr & FileAttributes.Hidden) != 0; + + var accessible = isFile || canAccess(s); + var isLeaf = isFile || !accessible || isEmptyFolder(s); + + var rawid = isFolder ? Util.AppendDirSeparator(s) : s; + if (skipFiles && !isFolder) + continue; + + if (!showHidden && isHidden) + continue; + + tn = new Serializable.TreeNode() + { + id = rawid, + text = SystemIO.IO_OS.PathGetFileName(s), + hidden = isHidden, + symlink = isSymlink, + iconCls = isFolder ? (accessible ? (isSymlink ? "x-tree-icon-symlink" : "x-tree-icon-parent") : "x-tree-icon-locked") : "x-tree-icon-leaf", + leaf = isLeaf + }; + } + catch + { + } + + if (tn != null) + yield return tn; + } + } + + public void POST(string key, RequestInfo info) + { + Process(key, info.Request.Form["path"].Value, info); + } + + public string Description { get { return "Enumerates the server filesystem"; } } + + public IEnumerable> Types + { + get + { + return new KeyValuePair[] { + new KeyValuePair(HttpServer.Method.Get, typeof(string[])), + }; + } + } + } +} + diff --git a/Duplicati/Server/WebServer/RESTMethods/Help.cs b/Duplicati.Library.RestAPI/RESTMethods/Help.cs similarity index 97% rename from Duplicati/Server/WebServer/RESTMethods/Help.cs rename to Duplicati.Library.RestAPI/RESTMethods/Help.cs index 87433390c..c99f97372 100644 --- a/Duplicati/Server/WebServer/RESTMethods/Help.cs +++ b/Duplicati.Library.RestAPI/RESTMethods/Help.cs @@ -1,100 +1,100 @@ -// Copyright (C) 2015, The Duplicati Team -// http://www.duplicati.com, info@duplicati.com -// -// This library is free software; you can redistribute it and/or modify -// it under the terms of the GNU Lesser General Public License as -// published by the Free Software Foundation; either version 2.1 of the -// License, or (at your option) any later version. -// -// This library is distributed in the hope that it will be useful, but -// WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU -// Lesser General Public License for more details. -// -// You should have received a copy of the GNU Lesser General Public -// License along with this library; if not, write to the Free Software -// Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA -using System; -using System.Linq; -using System.Text; -using Newtonsoft.Json; - -namespace Duplicati.Server.WebServer.RESTMethods -{ - public class Help : IRESTMethodGET - { - public void GET(string key, RequestInfo info) - { - var sb = new StringBuilder(); - if (string.IsNullOrWhiteSpace(key)) - { - foreach(var m in RESTHandler.Modules.Keys.OrderBy(x => x)) - { - var mod = RESTHandler.Modules[m]; - if (mod == this) - continue; - - var desc = mod.GetType().Name; - if (mod is IRESTMethodDocumented documented) - desc = documented.Description; - sb.AppendFormat(ITEM_TEMPLATE, RESTHandler.API_URI_PATH, m, mod.GetType().Name, desc); - } - - - var data = Encoding.UTF8.GetBytes(string.Format(TEMPLATE, "API Information", "", sb)); - - info.Response.ContentType = "text/html"; - info.Response.ContentLength = data.Length; - info.Response.Body.Write(data, 0, data.Length); - info.Response.Send(); - } - else - { - IRESTMethod m; - RESTHandler.Modules.TryGetValue(key, out m); - if (m == null) - { - info.Response.Status = System.Net.HttpStatusCode.NotFound; - info.Response.Reason = "Module not found"; - } - else - { - var desc = ""; - if (m is IRESTMethodDocumented doc) - { - desc = doc.Description; - foreach(var t in doc.Types) - sb.AppendFormat(METHOD_TEMPLATE, t.Key, JsonConvert.SerializeObject(t.Value)); //TODO: Format the type - } - - var data = Encoding.UTF8.GetBytes(string.Format(TEMPLATE, m.GetType().Name, desc, sb)); - - info.Response.ContentType = "text/html"; - info.Response.ContentLength = data.Length; - info.Response.Body.Write(data, 0, data.Length); - info.Response.Send(); - - } - } - } - - private const string TEMPLATE = @" -{0} - -

{0}

-

{1}

-
    -{2} -
- -"; - private const string ITEM_TEMPLATE = @" -
  • {2}: {3}
  • -"; - - private const string METHOD_TEMPLATE = @" -{0}:
    {1}
    -"; - } -} - +// Copyright (C) 2015, The Duplicati Team +// http://www.duplicati.com, info@duplicati.com +// +// This library is free software; you can redistribute it and/or modify +// it under the terms of the GNU Lesser General Public License as +// published by the Free Software Foundation; either version 2.1 of the +// License, or (at your option) any later version. +// +// This library is distributed in the hope that it will be useful, but +// WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +// Lesser General Public License for more details. +// +// You should have received a copy of the GNU Lesser General Public +// License along with this library; if not, write to the Free Software +// Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA +using System; +using System.Linq; +using System.Text; +using Newtonsoft.Json; + +namespace Duplicati.Server.WebServer.RESTMethods +{ + public class Help : IRESTMethodGET + { + public void GET(string key, RequestInfo info) + { + var sb = new StringBuilder(); + if (string.IsNullOrWhiteSpace(key)) + { + foreach(var m in RESTHandler.Modules.Keys.OrderBy(x => x)) + { + var mod = RESTHandler.Modules[m]; + if (mod == this) + continue; + + var desc = mod.GetType().Name; + if (mod is IRESTMethodDocumented documented) + desc = documented.Description; + sb.AppendFormat(ITEM_TEMPLATE, RESTHandler.API_URI_PATH, m, mod.GetType().Name, desc); + } + + + var data = Encoding.UTF8.GetBytes(string.Format(TEMPLATE, "API Information", "", sb)); + + info.Response.ContentType = "text/html"; + info.Response.ContentLength = data.Length; + info.Response.Body.Write(data, 0, data.Length); + info.Response.Send(); + } + else + { + IRESTMethod m; + RESTHandler.Modules.TryGetValue(key, out m); + if (m == null) + { + info.Response.Status = System.Net.HttpStatusCode.NotFound; + info.Response.Reason = "Module not found"; + } + else + { + var desc = ""; + if (m is IRESTMethodDocumented doc) + { + desc = doc.Description; + foreach(var t in doc.Types) + sb.AppendFormat(METHOD_TEMPLATE, t.Key, JsonConvert.SerializeObject(t.Value)); //TODO: Format the type + } + + var data = Encoding.UTF8.GetBytes(string.Format(TEMPLATE, m.GetType().Name, desc, sb)); + + info.Response.ContentType = "text/html"; + info.Response.ContentLength = data.Length; + info.Response.Body.Write(data, 0, data.Length); + info.Response.Send(); + + } + } + } + + private const string TEMPLATE = @" +{0} + +

    {0}

    +

    {1}

    +
      +{2} +
    + +"; + private const string ITEM_TEMPLATE = @" +
  • {2}: {3}
  • +"; + + private const string METHOD_TEMPLATE = @" +{0}:
    {1}
    +"; + } +} + diff --git a/Duplicati/Server/WebServer/RESTMethods/HyperV.cs b/Duplicati.Library.RestAPI/RESTMethods/HyperV.cs similarity index 97% rename from Duplicati/Server/WebServer/RESTMethods/HyperV.cs rename to Duplicati.Library.RestAPI/RESTMethods/HyperV.cs index b5298eab9..477ba28f5 100644 --- a/Duplicati/Server/WebServer/RESTMethods/HyperV.cs +++ b/Duplicati.Library.RestAPI/RESTMethods/HyperV.cs @@ -1,89 +1,89 @@ -// Copyright (C) 2015, The Duplicati Team -// http://www.duplicati.com, info@duplicati.com -// -// This library is free software; you can redistribute it and/or modify -// it under the terms of the GNU Lesser General Public License as -// published by the Free Software Foundation; either version 2.1 of the -// License, or (at your option) any later version. -// -// This library is distributed in the hope that it will be useful, but -// WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU -// Lesser General Public License for more details. -// -// You should have received a copy of the GNU Lesser General Public -// License along with this library; if not, write to the Free Software -// Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA -using System; -using System.Collections.Generic; -using Duplicati.Library.Interface; -using System.Linq; -using System.Security.Principal; -using Duplicati.Library.Snapshots; +// Copyright (C) 2015, The Duplicati Team +// http://www.duplicati.com, info@duplicati.com +// +// This library is free software; you can redistribute it and/or modify +// it under the terms of the GNU Lesser General Public License as +// published by the Free Software Foundation; either version 2.1 of the +// License, or (at your option) any later version. +// +// This library is distributed in the hope that it will be useful, but +// WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +// Lesser General Public License for more details. +// +// You should have received a copy of the GNU Lesser General Public +// License along with this library; if not, write to the Free Software +// Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA +using System; +using System.Collections.Generic; +using Duplicati.Library.Interface; +using System.Linq; +using System.Security.Principal; +using Duplicati.Library.Snapshots; using Duplicati.Library.Common; -namespace Duplicati.Server.WebServer.RESTMethods -{ - public class HyperV : IRESTMethodGET, IRESTMethodDocumented - { - public void GET(string key, RequestInfo info) - { - // Early exit in case we are non-windows to prevent attempting to load Windows-only components - if (Platform.IsClientWindows) - RealGET(key, info); - else - info.OutputOK(new string[0]); - } - - // Make sure the JIT does not attempt to inline this call and thus load - // referenced types from System.Management here - [System.Runtime.CompilerServices.MethodImpl(System.Runtime.CompilerServices.MethodImplOptions.NoInlining)] - private void RealGET(string key, RequestInfo info) - { - var hypervUtility = new HyperVUtility(); - - if (!hypervUtility.IsHyperVInstalled || !new WindowsPrincipal(WindowsIdentity.GetCurrent()).IsInRole(WindowsBuiltInRole.Administrator)) - { - info.OutputOK(new string[0]); - return; - } - - try - { - if (string.IsNullOrEmpty(key)) - { - hypervUtility.QueryHyperVGuestsInfo(); - info.OutputOK(hypervUtility.Guests.Select(x => new { id = x.ID, name = x.Name }).ToList()); - } - else - { - hypervUtility.QueryHyperVGuestsInfo(true); - var foundVMs = hypervUtility.Guests.FindAll(x => x.ID.Equals(new Guid(key))); - - if (foundVMs.Count == 1) - info.OutputOK(foundVMs[0].DataPaths.Select(x => new { text = x, id = x, cls = "folder", iconCls = "x-tree-icon-leaf", check = "false", leaf = "true" }).ToList()); - else - info.ReportClientError(string.Format("Cannot find VM with ID {0}.", key), System.Net.HttpStatusCode.NotFound); - } - } - catch (Exception ex) - { - info.ReportServerError("Failed to enumerate Hyper-V virtual machines: " + ex.Message); - } - } - - public string Description { get { return "Return a list of Hyper-V virtual machines"; } } - - public IEnumerable> Types - { - get - { - return new KeyValuePair[] { - new KeyValuePair(HttpServer.Method.Get, typeof(ICommandLineArgument[])) - }; - } - } - - } -} - +namespace Duplicati.Server.WebServer.RESTMethods +{ + public class HyperV : IRESTMethodGET, IRESTMethodDocumented + { + public void GET(string key, RequestInfo info) + { + // Early exit in case we are non-windows to prevent attempting to load Windows-only components + if (Platform.IsClientWindows) + RealGET(key, info); + else + info.OutputOK(new string[0]); + } + + // Make sure the JIT does not attempt to inline this call and thus load + // referenced types from System.Management here + [System.Runtime.CompilerServices.MethodImpl(System.Runtime.CompilerServices.MethodImplOptions.NoInlining)] + private void RealGET(string key, RequestInfo info) + { + var hypervUtility = new HyperVUtility(); + + if (!hypervUtility.IsHyperVInstalled || !new WindowsPrincipal(WindowsIdentity.GetCurrent()).IsInRole(WindowsBuiltInRole.Administrator)) + { + info.OutputOK(new string[0]); + return; + } + + try + { + if (string.IsNullOrEmpty(key)) + { + hypervUtility.QueryHyperVGuestsInfo(); + info.OutputOK(hypervUtility.Guests.Select(x => new { id = x.ID, name = x.Name }).ToList()); + } + else + { + hypervUtility.QueryHyperVGuestsInfo(true); + var foundVMs = hypervUtility.Guests.FindAll(x => x.ID.Equals(new Guid(key))); + + if (foundVMs.Count == 1) + info.OutputOK(foundVMs[0].DataPaths.Select(x => new { text = x, id = x, cls = "folder", iconCls = "x-tree-icon-leaf", check = "false", leaf = "true" }).ToList()); + else + info.ReportClientError(string.Format("Cannot find VM with ID {0}.", key), System.Net.HttpStatusCode.NotFound); + } + } + catch (Exception ex) + { + info.ReportServerError("Failed to enumerate Hyper-V virtual machines: " + ex.Message); + } + } + + public string Description { get { return "Return a list of Hyper-V virtual machines"; } } + + public IEnumerable> Types + { + get + { + return new KeyValuePair[] { + new KeyValuePair(HttpServer.Method.Get, typeof(ICommandLineArgument[])) + }; + } + } + + } +} + diff --git a/Duplicati/Server/WebServer/RESTMethods/IRESTMethod.cs b/Duplicati.Library.RestAPI/RESTMethods/IRESTMethod.cs similarity index 96% rename from Duplicati/Server/WebServer/RESTMethods/IRESTMethod.cs rename to Duplicati.Library.RestAPI/RESTMethods/IRESTMethod.cs index 024da32b0..94e7788d4 100644 --- a/Duplicati/Server/WebServer/RESTMethods/IRESTMethod.cs +++ b/Duplicati.Library.RestAPI/RESTMethods/IRESTMethod.cs @@ -1,56 +1,56 @@ -// Copyright (C) 2015, The Duplicati Team -// http://www.duplicati.com, info@duplicati.com -// -// This library is free software; you can redistribute it and/or modify -// it under the terms of the GNU Lesser General Public License as -// published by the Free Software Foundation; either version 2.1 of the -// License, or (at your option) any later version. -// -// This library is distributed in the hope that it will be useful, but -// WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU -// Lesser General Public License for more details. -// -// You should have received a copy of the GNU Lesser General Public -// License along with this library; if not, write to the Free Software -// Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA -using System; -using System.Collections.Generic; - -namespace Duplicati.Server.WebServer.RESTMethods -{ - public interface IRESTMethod - { - } - - public interface IRESTMethodDocumented - { - string Description { get; } - IEnumerable> Types { get; } - } - - public interface IRESTMethodGET : IRESTMethod - { - void GET(string key, RequestInfo info); - } - public interface IRESTMethodPUT : IRESTMethod - { - void PUT(string key, RequestInfo info); - } - - public interface IRESTMethodPOST : IRESTMethod - { - void POST(string key, RequestInfo info); - } - - public interface IRESTMethodDELETE : IRESTMethod - { - void DELETE(string key, RequestInfo info); - } - - public interface IRESTMethodPATCH : IRESTMethod - { - void PATCH(string key, RequestInfo info); - } -} - +// Copyright (C) 2015, The Duplicati Team +// http://www.duplicati.com, info@duplicati.com +// +// This library is free software; you can redistribute it and/or modify +// it under the terms of the GNU Lesser General Public License as +// published by the Free Software Foundation; either version 2.1 of the +// License, or (at your option) any later version. +// +// This library is distributed in the hope that it will be useful, but +// WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +// Lesser General Public License for more details. +// +// You should have received a copy of the GNU Lesser General Public +// License along with this library; if not, write to the Free Software +// Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA +using System; +using System.Collections.Generic; + +namespace Duplicati.Server.WebServer.RESTMethods +{ + public interface IRESTMethod + { + } + + public interface IRESTMethodDocumented + { + string Description { get; } + IEnumerable> Types { get; } + } + + public interface IRESTMethodGET : IRESTMethod + { + void GET(string key, RequestInfo info); + } + public interface IRESTMethodPUT : IRESTMethod + { + void PUT(string key, RequestInfo info); + } + + public interface IRESTMethodPOST : IRESTMethod + { + void POST(string key, RequestInfo info); + } + + public interface IRESTMethodDELETE : IRESTMethod + { + void DELETE(string key, RequestInfo info); + } + + public interface IRESTMethodPATCH : IRESTMethod + { + void PATCH(string key, RequestInfo info); + } +} + diff --git a/Duplicati/Server/WebServer/RESTMethods/Licenses.cs b/Duplicati.Library.RestAPI/RESTMethods/Licenses.cs similarity index 97% rename from Duplicati/Server/WebServer/RESTMethods/Licenses.cs rename to Duplicati.Library.RestAPI/RESTMethods/Licenses.cs index 893a7ae9d..b08749bce 100644 --- a/Duplicati/Server/WebServer/RESTMethods/Licenses.cs +++ b/Duplicati.Library.RestAPI/RESTMethods/Licenses.cs @@ -1,30 +1,30 @@ -// Copyright (C) 2015, The Duplicati Team -// http://www.duplicati.com, info@duplicati.com -// -// This library is free software; you can redistribute it and/or modify -// it under the terms of the GNU Lesser General Public License as -// published by the Free Software Foundation; either version 2.1 of the -// License, or (at your option) any later version. -// -// This library is distributed in the hope that it will be useful, but -// WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU -// Lesser General Public License for more details. -// -// You should have received a copy of the GNU Lesser General Public -// License along with this library; if not, write to the Free Software -// Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA -using System; - -namespace Duplicati.Server.WebServer.RESTMethods -{ - public class Licenses : IRESTMethodGET - { - public void GET(string key, RequestInfo info) - { - var path = System.IO.Path.Combine(System.IO.Path.GetDirectoryName(Duplicati.Library.Utility.Utility.getEntryAssembly().Location), "licenses"); - info.OutputOK(Duplicati.License.LicenseReader.ReadLicenses(path)); - } - } -} - +// Copyright (C) 2015, The Duplicati Team +// http://www.duplicati.com, info@duplicati.com +// +// This library is free software; you can redistribute it and/or modify +// it under the terms of the GNU Lesser General Public License as +// published by the Free Software Foundation; either version 2.1 of the +// License, or (at your option) any later version. +// +// This library is distributed in the hope that it will be useful, but +// WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +// Lesser General Public License for more details. +// +// You should have received a copy of the GNU Lesser General Public +// License along with this library; if not, write to the Free Software +// Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA +using System; + +namespace Duplicati.Server.WebServer.RESTMethods +{ + public class Licenses : IRESTMethodGET + { + public void GET(string key, RequestInfo info) + { + var path = System.IO.Path.Combine(System.IO.Path.GetDirectoryName(Duplicati.Library.Utility.Utility.getEntryAssembly().Location), "licenses"); + info.OutputOK(Duplicati.License.LicenseReader.ReadLicenses(path)); + } + } +} + diff --git a/Duplicati/Server/WebServer/RESTMethods/LogData.cs b/Duplicati.Library.RestAPI/RESTMethods/LogData.cs similarity index 94% rename from Duplicati/Server/WebServer/RESTMethods/LogData.cs rename to Duplicati.Library.RestAPI/RESTMethods/LogData.cs index f87bbe2cb..d1de17dc6 100644 --- a/Duplicati/Server/WebServer/RESTMethods/LogData.cs +++ b/Duplicati.Library.RestAPI/RESTMethods/LogData.cs @@ -1,117 +1,118 @@ -// Copyright (C) 2015, The Duplicati Team -// http://www.duplicati.com, info@duplicati.com -// -// This library is free software; you can redistribute it and/or modify -// it under the terms of the GNU Lesser General Public License as -// published by the Free Software Foundation; either version 2.1 of the -// License, or (at your option) any later version. -// -// This library is distributed in the hope that it will be useful, but -// WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU -// Lesser General Public License for more details. -// -// You should have received a copy of the GNU Lesser General Public -// License along with this library; if not, write to the Free Software -// Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA -using System; -using System.Collections.Generic; - -namespace Duplicati.Server.WebServer.RESTMethods -{ - public class LogData : IRESTMethodGET, IRESTMethodDocumented - { - public void GET(string key, RequestInfo info) - { - if ("poll".Equals(key, StringComparison.OrdinalIgnoreCase)) - { - var input = info.Request.QueryString; - var level_str = input["level"].Value ?? ""; - var id_str = input["id"].Value ?? ""; - - int pagesize; - if (!int.TryParse(info.Request.QueryString["pagesize"].Value, out pagesize)) - pagesize = 100; - - pagesize = Math.Max(1, Math.Min(500, pagesize)); - - Library.Logging.LogMessageType level; - long id; - - long.TryParse(id_str, out id); - Enum.TryParse(level_str, true, out level); - - info.OutputOK(Program.LogHandler.AfterID(id, level, pagesize)); - } - else - { - - List> res = null; - Program.DataConnection.ExecuteWithCommand(x => - { - res = DumpTable(x, "ErrorLog", "Timestamp", info.Request.QueryString["offset"].Value, info.Request.QueryString["pagesize"].Value); - }); - - info.OutputOK(res); - } - } - - - public static List> DumpTable(System.Data.IDbCommand cmd, string tablename, string pagingfield, string offset_str, string pagesize_str) - { - var result = new List>(); - - long pagesize; - if (!long.TryParse(pagesize_str, out pagesize)) - pagesize = 100; - - pagesize = Math.Max(10, Math.Min(500, pagesize)); - - cmd.CommandText = "SELECT * FROM \"" + tablename + "\""; - long offset = 0; - if (!string.IsNullOrWhiteSpace(offset_str) && long.TryParse(offset_str, out offset) && !string.IsNullOrEmpty(pagingfield)) - { - var p = cmd.CreateParameter(); - p.Value = offset; - cmd.Parameters.Add(p); - - cmd.CommandText += " WHERE \"" + pagingfield + "\" < ?"; - } - - if (!string.IsNullOrEmpty(pagingfield)) - cmd.CommandText += " ORDER BY \"" + pagingfield + "\" DESC"; - cmd.CommandText += " LIMIT " + pagesize.ToString(); - - using(var rd = cmd.ExecuteReader()) - { - var names = new List(); - for(var i = 0; i < rd.FieldCount; i++) - names.Add(rd.GetName(i)); - - while (rd.Read()) - { - var dict = new Dictionary(); - for(int i = 0; i < names.Count; i++) - dict[names[i]] = rd.GetValue(i); - - result.Add(dict); - } - } - - return result; - } - - public string Description { get { return "Retrieves system log data"; } } - - public IEnumerable> Types - { - get - { - return new KeyValuePair[] { - new KeyValuePair(HttpServer.Method.Get, typeof(Dictionary[])), - }; - } - } - } -} - +// Copyright (C) 2015, The Duplicati Team +// http://www.duplicati.com, info@duplicati.com +// +// This library is free software; you can redistribute it and/or modify +// it under the terms of the GNU Lesser General Public License as +// published by the Free Software Foundation; either version 2.1 of the +// License, or (at your option) any later version. +// +// This library is distributed in the hope that it will be useful, but +// WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +// Lesser General Public License for more details. +// +// You should have received a copy of the GNU Lesser General Public +// License along with this library; if not, write to the Free Software +// Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA +using System; +using System.Collections.Generic; +using Duplicati.Library.RestAPI; + +namespace Duplicati.Server.WebServer.RESTMethods +{ + public class LogData : IRESTMethodGET, IRESTMethodDocumented + { + public void GET(string key, RequestInfo info) + { + if ("poll".Equals(key, StringComparison.OrdinalIgnoreCase)) + { + var input = info.Request.QueryString; + var level_str = input["level"].Value ?? ""; + var id_str = input["id"].Value ?? ""; + + int pagesize; + if (!int.TryParse(info.Request.QueryString["pagesize"].Value, out pagesize)) + pagesize = 100; + + pagesize = Math.Max(1, Math.Min(500, pagesize)); + + Library.Logging.LogMessageType level; + long id; + + long.TryParse(id_str, out id); + Enum.TryParse(level_str, true, out level); + + info.OutputOK(FIXMEGlobal.LogHandler.AfterID(id, level, pagesize)); + } + else + { + + List> res = null; + FIXMEGlobal.DataConnection.ExecuteWithCommand(x => + { + res = DumpTable(x, "ErrorLog", "Timestamp", info.Request.QueryString["offset"].Value, info.Request.QueryString["pagesize"].Value); + }); + + info.OutputOK(res); + } + } + + + public static List> DumpTable(System.Data.IDbCommand cmd, string tablename, string pagingfield, string offset_str, string pagesize_str) + { + var result = new List>(); + + long pagesize; + if (!long.TryParse(pagesize_str, out pagesize)) + pagesize = 100; + + pagesize = Math.Max(10, Math.Min(500, pagesize)); + + cmd.CommandText = "SELECT * FROM \"" + tablename + "\""; + long offset = 0; + if (!string.IsNullOrWhiteSpace(offset_str) && long.TryParse(offset_str, out offset) && !string.IsNullOrEmpty(pagingfield)) + { + var p = cmd.CreateParameter(); + p.Value = offset; + cmd.Parameters.Add(p); + + cmd.CommandText += " WHERE \"" + pagingfield + "\" < ?"; + } + + if (!string.IsNullOrEmpty(pagingfield)) + cmd.CommandText += " ORDER BY \"" + pagingfield + "\" DESC"; + cmd.CommandText += " LIMIT " + pagesize.ToString(); + + using(var rd = cmd.ExecuteReader()) + { + var names = new List(); + for(var i = 0; i < rd.FieldCount; i++) + names.Add(rd.GetName(i)); + + while (rd.Read()) + { + var dict = new Dictionary(); + for(int i = 0; i < names.Count; i++) + dict[names[i]] = rd.GetValue(i); + + result.Add(dict); + } + } + + return result; + } + + public string Description { get { return "Retrieves system log data"; } } + + public IEnumerable> Types + { + get + { + return new KeyValuePair[] { + new KeyValuePair(HttpServer.Method.Get, typeof(Dictionary[])), + }; + } + } + } +} + diff --git a/Duplicati/Server/WebServer/RESTMethods/MSSQL.cs b/Duplicati.Library.RestAPI/RESTMethods/MSSQL.cs similarity index 97% rename from Duplicati/Server/WebServer/RESTMethods/MSSQL.cs rename to Duplicati.Library.RestAPI/RESTMethods/MSSQL.cs index 2015fb959..33c64bc12 100644 --- a/Duplicati/Server/WebServer/RESTMethods/MSSQL.cs +++ b/Duplicati.Library.RestAPI/RESTMethods/MSSQL.cs @@ -1,87 +1,87 @@ -// Copyright (C) 2015, The Duplicati Team -// http://www.duplicati.com, info@duplicati.com -// -// This library is free software; you can redistribute it and/or modify -// it under the terms of the GNU Lesser General Public License as -// published by the Free Software Foundation; either version 2.1 of the -// License, or (at your option) any later version. -// -// This library is distributed in the hope that it will be useful, but -// WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU -// Lesser General Public License for more details. -// -// You should have received a copy of the GNU Lesser General Public -// License along with this library; if not, write to the Free Software -// Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA -using System; -using System.Collections.Generic; -using Duplicati.Library.Interface; -using System.Linq; -using System.Security.Principal; -using Duplicati.Library.Snapshots; +// Copyright (C) 2015, The Duplicati Team +// http://www.duplicati.com, info@duplicati.com +// +// This library is free software; you can redistribute it and/or modify +// it under the terms of the GNU Lesser General Public License as +// published by the Free Software Foundation; either version 2.1 of the +// License, or (at your option) any later version. +// +// This library is distributed in the hope that it will be useful, but +// WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +// Lesser General Public License for more details. +// +// You should have received a copy of the GNU Lesser General Public +// License along with this library; if not, write to the Free Software +// Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA +using System; +using System.Collections.Generic; +using Duplicati.Library.Interface; +using System.Linq; +using System.Security.Principal; +using Duplicati.Library.Snapshots; using Duplicati.Library.Common; -namespace Duplicati.Server.WebServer.RESTMethods -{ - public class MSSQL : IRESTMethodGET, IRESTMethodDocumented - { - public void GET(string key, RequestInfo info) - { - // Early exit in case we are non-windows to prevent attempting to load Windows-only components - if (Platform.IsClientWindows) - RealGET(key, info); - else - info.OutputOK(new string[0]); - } - - // Make sure the JIT does not attempt to inline this call and thus load - // referenced types from System.Management here - [System.Runtime.CompilerServices.MethodImpl(System.Runtime.CompilerServices.MethodImplOptions.NoInlining)] - private void RealGET(string key, RequestInfo info) - { - var mssqlUtility = new MSSQLUtility(); - - if (!mssqlUtility.IsMSSQLInstalled || !new WindowsPrincipal(WindowsIdentity.GetCurrent()).IsInRole(WindowsBuiltInRole.Administrator)) - { - info.OutputOK(new string[0]); - return; - } - - try - { - mssqlUtility.QueryDBsInfo(); - - if (string.IsNullOrEmpty(key)) - info.OutputOK(mssqlUtility.DBs.Select(x => new { id = x.ID, name = x.Name }).ToList()); - else - { - var foundDBs = mssqlUtility.DBs.FindAll(x => x.ID.Equals(key, StringComparison.OrdinalIgnoreCase)); - - if (foundDBs.Count == 1) - info.OutputOK(foundDBs[0].DataPaths.Select(x => new { text = x, id = x, cls = "folder", iconCls = "x-tree-icon-leaf", check = "false", leaf = "true" }).ToList()); - else - info.ReportClientError(string.Format("Cannot find DB with ID {0}.", key), System.Net.HttpStatusCode.NotFound); - } - } - catch (Exception ex) - { - info.ReportServerError("Failed to enumerate Microsoft SQL Server databases: " + ex.Message); - } - } - - public string Description { get { return "Return a list of Microsoft SQL Server databases"; } } - - public IEnumerable> Types - { - get - { - return new KeyValuePair[] { - new KeyValuePair(HttpServer.Method.Get, typeof(ICommandLineArgument[])) - }; - } - } - - } -} - +namespace Duplicati.Server.WebServer.RESTMethods +{ + public class MSSQL : IRESTMethodGET, IRESTMethodDocumented + { + public void GET(string key, RequestInfo info) + { + // Early exit in case we are non-windows to prevent attempting to load Windows-only components + if (Platform.IsClientWindows) + RealGET(key, info); + else + info.OutputOK(new string[0]); + } + + // Make sure the JIT does not attempt to inline this call and thus load + // referenced types from System.Management here + [System.Runtime.CompilerServices.MethodImpl(System.Runtime.CompilerServices.MethodImplOptions.NoInlining)] + private void RealGET(string key, RequestInfo info) + { + var mssqlUtility = new MSSQLUtility(); + + if (!mssqlUtility.IsMSSQLInstalled || !new WindowsPrincipal(WindowsIdentity.GetCurrent()).IsInRole(WindowsBuiltInRole.Administrator)) + { + info.OutputOK(new string[0]); + return; + } + + try + { + mssqlUtility.QueryDBsInfo(); + + if (string.IsNullOrEmpty(key)) + info.OutputOK(mssqlUtility.DBs.Select(x => new { id = x.ID, name = x.Name }).ToList()); + else + { + var foundDBs = mssqlUtility.DBs.FindAll(x => x.ID.Equals(key, StringComparison.OrdinalIgnoreCase)); + + if (foundDBs.Count == 1) + info.OutputOK(foundDBs[0].DataPaths.Select(x => new { text = x, id = x, cls = "folder", iconCls = "x-tree-icon-leaf", check = "false", leaf = "true" }).ToList()); + else + info.ReportClientError(string.Format("Cannot find DB with ID {0}.", key), System.Net.HttpStatusCode.NotFound); + } + } + catch (Exception ex) + { + info.ReportServerError("Failed to enumerate Microsoft SQL Server databases: " + ex.Message); + } + } + + public string Description { get { return "Return a list of Microsoft SQL Server databases"; } } + + public IEnumerable> Types + { + get + { + return new KeyValuePair[] { + new KeyValuePair(HttpServer.Method.Get, typeof(ICommandLineArgument[])) + }; + } + } + + } +} + diff --git a/Duplicati/Server/WebServer/RESTMethods/Notification.cs b/Duplicati.Library.RestAPI/RESTMethods/Notification.cs similarity index 86% rename from Duplicati/Server/WebServer/RESTMethods/Notification.cs rename to Duplicati.Library.RestAPI/RESTMethods/Notification.cs index 14d23bdbc..9473f248b 100644 --- a/Duplicati/Server/WebServer/RESTMethods/Notification.cs +++ b/Duplicati.Library.RestAPI/RESTMethods/Notification.cs @@ -1,60 +1,61 @@ -// Copyright (C) 2015, The Duplicati Team -// http://www.duplicati.com, info@duplicati.com -// -// This library is free software; you can redistribute it and/or modify -// it under the terms of the GNU Lesser General Public License as -// published by the Free Software Foundation; either version 2.1 of the -// License, or (at your option) any later version. -// -// This library is distributed in the hope that it will be useful, but -// WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU -// Lesser General Public License for more details. -// -// You should have received a copy of the GNU Lesser General Public -// License along with this library; if not, write to the Free Software -// Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA -using System; -using System.Linq; - -namespace Duplicati.Server.WebServer.RESTMethods -{ - public class Notification : IRESTMethodGET, IRESTMethodDELETE - { - public void GET(string key, RequestInfo info) - { - long id; - if (!long.TryParse(key, out id)) - { - info.ReportClientError("Invalid ID", System.Net.HttpStatusCode.BadRequest); - return; - } - - var el = Program.DataConnection.GetNotifications().FirstOrDefault(x => x.ID == id); - if (el == null) - info.ReportClientError("No such notification", System.Net.HttpStatusCode.NotFound); - else - info.OutputOK(el); - } - - public void DELETE(string key, RequestInfo info) - { - long id; - if (!long.TryParse(key, out id)) - { - info.ReportClientError("Invalid ID", System.Net.HttpStatusCode.BadRequest); - return; - } - - var el = Program.DataConnection.GetNotifications().FirstOrDefault(x => x.ID == id); - if (el == null) - info.ReportClientError("No such notification", System.Net.HttpStatusCode.NotFound); - else - { - Program.DataConnection.DismissNotification(id); - info.OutputOK(); - } - } - } -} - +// Copyright (C) 2015, The Duplicati Team +// http://www.duplicati.com, info@duplicati.com +// +// This library is free software; you can redistribute it and/or modify +// it under the terms of the GNU Lesser General Public License as +// published by the Free Software Foundation; either version 2.1 of the +// License, or (at your option) any later version. +// +// This library is distributed in the hope that it will be useful, but +// WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +// Lesser General Public License for more details. +// +// You should have received a copy of the GNU Lesser General Public +// License along with this library; if not, write to the Free Software +// Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA +using Duplicati.Library.RestAPI; +using System; +using System.Linq; + +namespace Duplicati.Server.WebServer.RESTMethods +{ + public class Notification : IRESTMethodGET, IRESTMethodDELETE + { + public void GET(string key, RequestInfo info) + { + long id; + if (!long.TryParse(key, out id)) + { + info.ReportClientError("Invalid ID", System.Net.HttpStatusCode.BadRequest); + return; + } + + var el = FIXMEGlobal.DataConnection.GetNotifications().FirstOrDefault(x => x.ID == id); + if (el == null) + info.ReportClientError("No such notification", System.Net.HttpStatusCode.NotFound); + else + info.OutputOK(el); + } + + public void DELETE(string key, RequestInfo info) + { + long id; + if (!long.TryParse(key, out id)) + { + info.ReportClientError("Invalid ID", System.Net.HttpStatusCode.BadRequest); + return; + } + + var el = FIXMEGlobal.DataConnection.GetNotifications().FirstOrDefault(x => x.ID == id); + if (el == null) + info.ReportClientError("No such notification", System.Net.HttpStatusCode.NotFound); + else + { + FIXMEGlobal.DataConnection.DismissNotification(id); + info.OutputOK(); + } + } + } +} + diff --git a/Duplicati/Server/WebServer/RESTMethods/Notifications.cs b/Duplicati.Library.RestAPI/RESTMethods/Notifications.cs similarity index 90% rename from Duplicati/Server/WebServer/RESTMethods/Notifications.cs rename to Duplicati.Library.RestAPI/RESTMethods/Notifications.cs index cbc76032d..6578dbf32 100644 --- a/Duplicati/Server/WebServer/RESTMethods/Notifications.cs +++ b/Duplicati.Library.RestAPI/RESTMethods/Notifications.cs @@ -1,29 +1,30 @@ -// Copyright (C) 2015, The Duplicati Team -// http://www.duplicati.com, info@duplicati.com -// -// This library is free software; you can redistribute it and/or modify -// it under the terms of the GNU Lesser General Public License as -// published by the Free Software Foundation; either version 2.1 of the -// License, or (at your option) any later version. -// -// This library is distributed in the hope that it will be useful, but -// WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU -// Lesser General Public License for more details. -// -// You should have received a copy of the GNU Lesser General Public -// License along with this library; if not, write to the Free Software -// Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA -using System; - -namespace Duplicati.Server.WebServer.RESTMethods -{ - public class Notifications : IRESTMethodGET - { - public void GET(string key, RequestInfo info) - { - info.OutputOK(Program.DataConnection.GetNotifications()); - } - } -} - +// Copyright (C) 2015, The Duplicati Team +// http://www.duplicati.com, info@duplicati.com +// +// This library is free software; you can redistribute it and/or modify +// it under the terms of the GNU Lesser General Public License as +// published by the Free Software Foundation; either version 2.1 of the +// License, or (at your option) any later version. +// +// This library is distributed in the hope that it will be useful, but +// WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +// Lesser General Public License for more details. +// +// You should have received a copy of the GNU Lesser General Public +// License along with this library; if not, write to the Free Software +// Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA +using Duplicati.Library.RestAPI; +using System; + +namespace Duplicati.Server.WebServer.RESTMethods +{ + public class Notifications : IRESTMethodGET + { + public void GET(string key, RequestInfo info) + { + info.OutputOK(FIXMEGlobal.DataConnection.GetNotifications()); + } + } +} + diff --git a/Duplicati/Server/WebServer/RESTMethods/ProgressState.cs b/Duplicati.Library.RestAPI/RESTMethods/ProgressState.cs similarity index 91% rename from Duplicati/Server/WebServer/RESTMethods/ProgressState.cs rename to Duplicati.Library.RestAPI/RESTMethods/ProgressState.cs index 4a757df2e..18deb5a70 100644 --- a/Duplicati/Server/WebServer/RESTMethods/ProgressState.cs +++ b/Duplicati.Library.RestAPI/RESTMethods/ProgressState.cs @@ -1,45 +1,46 @@ -// Copyright (C) 2015, The Duplicati Team -// http://www.duplicati.com, info@duplicati.com -// -// This library is free software; you can redistribute it and/or modify -// it under the terms of the GNU Lesser General Public License as -// published by the Free Software Foundation; either version 2.1 of the -// License, or (at your option) any later version. -// -// This library is distributed in the hope that it will be useful, but -// WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU -// Lesser General Public License for more details. -// -// You should have received a copy of the GNU Lesser General Public -// License along with this library; if not, write to the Free Software -// Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA -using System; -using System.Collections.Generic; - -namespace Duplicati.Server.WebServer.RESTMethods -{ - public class ProgressState : IRESTMethodGET, IRESTMethodDocumented - { - public void GET(string key, RequestInfo info) - { - if (Program.GenerateProgressState == null) - info.ReportClientError("No active backup", System.Net.HttpStatusCode.NotFound); - else - info.OutputOK(Program.GenerateProgressState()); - } - - public string Description { get { return "Return the progress of the currently running operation."; } } - - public IEnumerable> Types - { - get - { - return new KeyValuePair[] { - new KeyValuePair(HttpServer.Method.Get, typeof(Serialization.Interface.IProgressEventData)) - }; - } - } - } -} - +// Copyright (C) 2015, The Duplicati Team +// http://www.duplicati.com, info@duplicati.com +// +// This library is free software; you can redistribute it and/or modify +// it under the terms of the GNU Lesser General Public License as +// published by the Free Software Foundation; either version 2.1 of the +// License, or (at your option) any later version. +// +// This library is distributed in the hope that it will be useful, but +// WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +// Lesser General Public License for more details. +// +// You should have received a copy of the GNU Lesser General Public +// License along with this library; if not, write to the Free Software +// Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA +using Duplicati.Library.RestAPI; +using System; +using System.Collections.Generic; + +namespace Duplicati.Server.WebServer.RESTMethods +{ + public class ProgressState : IRESTMethodGET, IRESTMethodDocumented + { + public void GET(string key, RequestInfo info) + { + if (FIXMEGlobal.GenerateProgressState == null) + info.ReportClientError("No active backup", System.Net.HttpStatusCode.NotFound); + else + info.OutputOK(FIXMEGlobal.GenerateProgressState()); + } + + public string Description { get { return "Return the progress of the currently running operation."; } } + + public IEnumerable> Types + { + get + { + return new KeyValuePair[] { + new KeyValuePair(HttpServer.Method.Get, typeof(Serialization.Interface.IProgressEventData)) + }; + } + } + } +} + diff --git a/Duplicati/Server/WebServer/RESTMethods/RemoteOperation.cs b/Duplicati.Library.RestAPI/RESTMethods/RemoteOperation.cs similarity index 97% rename from Duplicati/Server/WebServer/RESTMethods/RemoteOperation.cs rename to Duplicati.Library.RestAPI/RESTMethods/RemoteOperation.cs index 6996683f6..7f165af89 100644 --- a/Duplicati/Server/WebServer/RESTMethods/RemoteOperation.cs +++ b/Duplicati.Library.RestAPI/RESTMethods/RemoteOperation.cs @@ -1,187 +1,187 @@ -// Copyright (C) 2015, The Duplicati Team -// http://www.duplicati.com, info@duplicati.com -// -// This library is free software; you can redistribute it and/or modify -// it under the terms of the GNU Lesser General Public License as -// published by the Free Software Foundation; either version 2.1 of the -// License, or (at your option) any later version. -// -// This library is distributed in the hope that it will be useful, but -// WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU -// Lesser General Public License for more details. -// -// You should have received a copy of the GNU Lesser General Public -// License along with this library; if not, write to the Free Software -// Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA -using System; -using System.Collections.Generic; -using System.Linq; -using System.Threading; - -namespace Duplicati.Server.WebServer.RESTMethods -{ - public class RemoteOperation : IRESTMethodGET, IRESTMethodPOST - { - private void LocateDbUri(string uri, RequestInfo info) - { - var path = Library.Main.DatabaseLocator.GetDatabasePath(uri, null, false, false); - info.OutputOK(new { - Exists = !string.IsNullOrWhiteSpace(path), - Path = path - }); - } - - private void CreateFolder(string uri, RequestInfo info) - { - using(var b = Duplicati.Library.DynamicLoader.BackendLoader.GetBackend(uri, new Dictionary())) - b.CreateFolder(); - - info.OutputOK(); - } - - private void UploadFile(string uri, RequestInfo info) - { - var data = info.Request.QueryString["data"].Value; - var remotename = info.Request.QueryString["filename"].Value; - - using(var ms = new System.IO.MemoryStream()) - using(var b = Library.DynamicLoader.BackendLoader.GetBackend(uri, new Dictionary())) - { - using(var tf = new Library.Utility.TempFile()) - { - System.IO.File.WriteAllText(tf, data); - b.PutAsync(remotename, tf, CancellationToken.None).Wait(); - } - } - - info.OutputOK(); - } - - private void ListFolder(string uri, RequestInfo info) - { - using(var b = Duplicati.Library.DynamicLoader.BackendLoader.GetBackend(uri, new Dictionary())) - info.OutputOK(b.List()); - } - - private void TestConnection(string url, RequestInfo info) - { - - var modules = (from n in Library.DynamicLoader.GenericLoader.Modules - where n is Library.Interface.IConnectionModule - select n).ToArray(); - - try - { - var uri = new Library.Utility.Uri(url); - var qp = uri.QueryParameters; - - var opts = new Dictionary(); - foreach (var k in qp.Keys.Cast()) - opts[k] = qp[k]; - - foreach (var n in modules) - n.Configure(opts); - - using (var b = Duplicati.Library.DynamicLoader.BackendLoader.GetBackend(url, new Dictionary())) - b.Test(); - - info.OutputOK(); - } - catch (Duplicati.Library.Interface.FolderMissingException) - { - info.ReportServerError("missing-folder"); - } - catch (Duplicati.Library.Utility.SslCertificateValidator.InvalidCertificateException icex) - { - if (string.IsNullOrWhiteSpace(icex.Certificate)) - info.ReportServerError(icex.Message); - else - info.ReportServerError("incorrect-cert:" + icex.Certificate); - } - catch (Duplicati.Library.Utility.HostKeyException hex) - { - if (string.IsNullOrWhiteSpace(hex.ReportedHostKey)) - info.ReportServerError(hex.Message); - else - { - info.ReportServerError(string.Format( - @"incorrect-host-key:""{0}"", accepted-host-key:""{1}""", - hex.ReportedHostKey, - hex.AcceptedHostKey - )); - } - } - finally - { - foreach (var n in modules) - if (n is IDisposable disposable) - disposable.Dispose(); - } - } - - public void GET(string key, RequestInfo info) - { - var parts = (key ?? "").Split(new char[] { '/' }, 2); - - if (parts.Length <= 1) - { - info.ReportClientError("No url or operation supplied", System.Net.HttpStatusCode.BadRequest); - return; - } - - var url = Library.Utility.Uri.UrlDecode(parts.First()); - var operation = parts.Last().ToLowerInvariant(); - - switch (operation) - { - case "dbpath": - LocateDbUri(url, info); - return; - case "list": - ListFolder(url, info); - return; - case "create": - CreateFolder(url, info); - return; - case "test": - TestConnection(url, info); - return; - default: - info.ReportClientError("No such method", System.Net.HttpStatusCode.BadRequest); - return; - } - } - - public void POST(string key, RequestInfo info) - { - string url; - - using(var sr = new System.IO.StreamReader(info.Request.Body, System.Text.Encoding.UTF8, true)) - url = sr.ReadToEnd(); - - switch (key) - { - case "dbpath": - LocateDbUri(url, info); - return; - case "list": - ListFolder(url, info); - return; - case "create": - CreateFolder(url, info); - return; - case "put": - UploadFile(url, info); - return; - case "test": - TestConnection(url, info); - return; - default: - info.ReportClientError("No such method", System.Net.HttpStatusCode.BadRequest); - return; - } - } - } -} - +// Copyright (C) 2015, The Duplicati Team +// http://www.duplicati.com, info@duplicati.com +// +// This library is free software; you can redistribute it and/or modify +// it under the terms of the GNU Lesser General Public License as +// published by the Free Software Foundation; either version 2.1 of the +// License, or (at your option) any later version. +// +// This library is distributed in the hope that it will be useful, but +// WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +// Lesser General Public License for more details. +// +// You should have received a copy of the GNU Lesser General Public +// License along with this library; if not, write to the Free Software +// Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading; + +namespace Duplicati.Server.WebServer.RESTMethods +{ + public class RemoteOperation : IRESTMethodGET, IRESTMethodPOST + { + private void LocateDbUri(string uri, RequestInfo info) + { + var path = Library.Main.DatabaseLocator.GetDatabasePath(uri, null, false, false); + info.OutputOK(new { + Exists = !string.IsNullOrWhiteSpace(path), + Path = path + }); + } + + private void CreateFolder(string uri, RequestInfo info) + { + using(var b = Duplicati.Library.DynamicLoader.BackendLoader.GetBackend(uri, new Dictionary())) + b.CreateFolder(); + + info.OutputOK(); + } + + private void UploadFile(string uri, RequestInfo info) + { + var data = info.Request.QueryString["data"].Value; + var remotename = info.Request.QueryString["filename"].Value; + + using(var ms = new System.IO.MemoryStream()) + using(var b = Library.DynamicLoader.BackendLoader.GetBackend(uri, new Dictionary())) + { + using(var tf = new Library.Utility.TempFile()) + { + System.IO.File.WriteAllText(tf, data); + b.PutAsync(remotename, tf, CancellationToken.None).Wait(); + } + } + + info.OutputOK(); + } + + private void ListFolder(string uri, RequestInfo info) + { + using(var b = Duplicati.Library.DynamicLoader.BackendLoader.GetBackend(uri, new Dictionary())) + info.OutputOK(b.List()); + } + + private void TestConnection(string url, RequestInfo info) + { + + var modules = (from n in Library.DynamicLoader.GenericLoader.Modules + where n is Library.Interface.IConnectionModule + select n).ToArray(); + + try + { + var uri = new Library.Utility.Uri(url); + var qp = uri.QueryParameters; + + var opts = new Dictionary(); + foreach (var k in qp.Keys.Cast()) + opts[k] = qp[k]; + + foreach (var n in modules) + n.Configure(opts); + + using (var b = Duplicati.Library.DynamicLoader.BackendLoader.GetBackend(url, new Dictionary())) + b.Test(); + + info.OutputOK(); + } + catch (Duplicati.Library.Interface.FolderMissingException) + { + info.ReportServerError("missing-folder"); + } + catch (Duplicati.Library.Utility.SslCertificateValidator.InvalidCertificateException icex) + { + if (string.IsNullOrWhiteSpace(icex.Certificate)) + info.ReportServerError(icex.Message); + else + info.ReportServerError("incorrect-cert:" + icex.Certificate); + } + catch (Duplicati.Library.Utility.HostKeyException hex) + { + if (string.IsNullOrWhiteSpace(hex.ReportedHostKey)) + info.ReportServerError(hex.Message); + else + { + info.ReportServerError(string.Format( + @"incorrect-host-key:""{0}"", accepted-host-key:""{1}""", + hex.ReportedHostKey, + hex.AcceptedHostKey + )); + } + } + finally + { + foreach (var n in modules) + if (n is IDisposable disposable) + disposable.Dispose(); + } + } + + public void GET(string key, RequestInfo info) + { + var parts = (key ?? "").Split(new char[] { '/' }, 2); + + if (parts.Length <= 1) + { + info.ReportClientError("No url or operation supplied", System.Net.HttpStatusCode.BadRequest); + return; + } + + var url = Library.Utility.Uri.UrlDecode(parts.First()); + var operation = parts.Last().ToLowerInvariant(); + + switch (operation) + { + case "dbpath": + LocateDbUri(url, info); + return; + case "list": + ListFolder(url, info); + return; + case "create": + CreateFolder(url, info); + return; + case "test": + TestConnection(url, info); + return; + default: + info.ReportClientError("No such method", System.Net.HttpStatusCode.BadRequest); + return; + } + } + + public void POST(string key, RequestInfo info) + { + string url; + + using(var sr = new System.IO.StreamReader(info.Request.Body, System.Text.Encoding.UTF8, true)) + url = sr.ReadToEnd(); + + switch (key) + { + case "dbpath": + LocateDbUri(url, info); + return; + case "list": + ListFolder(url, info); + return; + case "create": + CreateFolder(url, info); + return; + case "put": + UploadFile(url, info); + return; + case "test": + TestConnection(url, info); + return; + default: + info.ReportClientError("No such method", System.Net.HttpStatusCode.BadRequest); + return; + } + } + } +} + diff --git a/Duplicati/Server/WebServer/RESTMethods/RequestInfo.cs b/Duplicati.Library.RestAPI/RESTMethods/RequestInfo.cs similarity index 97% rename from Duplicati/Server/WebServer/RESTMethods/RequestInfo.cs rename to Duplicati.Library.RestAPI/RESTMethods/RequestInfo.cs index c80b7edd8..fa38fc5df 100644 --- a/Duplicati/Server/WebServer/RESTMethods/RequestInfo.cs +++ b/Duplicati.Library.RestAPI/RESTMethods/RequestInfo.cs @@ -1,109 +1,109 @@ -// Copyright (C) 2015, The Duplicati Team -// http://www.duplicati.com, info@duplicati.com -// -// This library is free software; you can redistribute it and/or modify -// it under the terms of the GNU Lesser General Public License as -// published by the Free Software Foundation; either version 2.1 of the -// License, or (at your option) any later version. -// -// This library is distributed in the hope that it will be useful, but -// WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU -// Lesser General Public License for more details. -// -// You should have received a copy of the GNU Lesser General Public -// License along with this library; if not, write to the Free Software -// Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA -using System; - -namespace Duplicati.Server.WebServer.RESTMethods -{ - public class RequestInfo : IDisposable - { - public HttpServer.IHttpRequest Request { get; private set; } - public HttpServer.IHttpResponse Response { get; private set; } - public HttpServer.Sessions.IHttpSession Session { get; private set; } - public BodyWriter BodyWriter { get; private set; } - public RequestInfo(HttpServer.IHttpRequest request, HttpServer.IHttpResponse response, HttpServer.Sessions.IHttpSession session) - { - Request = request; - Response = response; - Session = session; - BodyWriter = new BodyWriter(response, request); - } - - public void ReportServerError(string message, System.Net.HttpStatusCode code = System.Net.HttpStatusCode.InternalServerError) - { - Response.Status = code; - Response.Reason = message; - - BodyWriter.WriteJsonObject(new { Error = message }); - } - - public void ReportClientError(string message, System.Net.HttpStatusCode code = System.Net.HttpStatusCode.BadRequest) - { - ReportServerError(message, code); - } - - public bool LongPollCheck(EventPollNotify poller, ref long id, out bool isError) - { - HttpServer.HttpInput input = String.Equals(Request.Method, "POST", StringComparison.OrdinalIgnoreCase) ? Request.Form : Request.QueryString; - if (Library.Utility.Utility.ParseBool(input["longpoll"].Value, false)) - { - long lastEventId; - if (!long.TryParse(input["lasteventid"].Value, out lastEventId)) - { - ReportClientError("When activating long poll, the request must include the last event id", System.Net.HttpStatusCode.BadRequest); - isError = true; - return false; - } - - TimeSpan ts; - try { ts = Library.Utility.Timeparser.ParseTimeSpan(input["duration"].Value); } - catch (Exception ex) - { - ReportClientError("Invalid duration: " + ex.Message, System.Net.HttpStatusCode.BadRequest); - isError = true; - return false; - } - - if (ts <= TimeSpan.FromSeconds(10) || ts.TotalMilliseconds > int.MaxValue) - { - ReportClientError("Invalid duration, must be at least 10 seconds, and less than " + int.MaxValue + " milliseconds", System.Net.HttpStatusCode.BadRequest); - isError = true; - return false; - } - - isError = false; - id = poller.Wait(lastEventId, (int)ts.TotalMilliseconds); - return true; - } - - isError = false; - return false; - } - - public void OutputOK(object item = null) - { - BodyWriter.OutputOK(item); - } - - public void OutputError(object item = null, System.Net.HttpStatusCode code = System.Net.HttpStatusCode.InternalServerError, string reason = null) - { - Response.Status = code; - Response.Reason = reason ?? "Error"; - BodyWriter.WriteJsonObject(item); - } - - public void Dispose() - { - if (BodyWriter != null) - { - var bw = BodyWriter; - BodyWriter = null; - bw.Dispose(); - } - } - } -} - +// Copyright (C) 2015, The Duplicati Team +// http://www.duplicati.com, info@duplicati.com +// +// This library is free software; you can redistribute it and/or modify +// it under the terms of the GNU Lesser General Public License as +// published by the Free Software Foundation; either version 2.1 of the +// License, or (at your option) any later version. +// +// This library is distributed in the hope that it will be useful, but +// WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +// Lesser General Public License for more details. +// +// You should have received a copy of the GNU Lesser General Public +// License along with this library; if not, write to the Free Software +// Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA +using System; + +namespace Duplicati.Server.WebServer.RESTMethods +{ + public class RequestInfo : IDisposable + { + public HttpServer.IHttpRequest Request { get; private set; } + public HttpServer.IHttpResponse Response { get; private set; } + public HttpServer.Sessions.IHttpSession Session { get; private set; } + public BodyWriter BodyWriter { get; private set; } + public RequestInfo(HttpServer.IHttpRequest request, HttpServer.IHttpResponse response, HttpServer.Sessions.IHttpSession session) + { + Request = request; + Response = response; + Session = session; + BodyWriter = new BodyWriter(response, request); + } + + public void ReportServerError(string message, System.Net.HttpStatusCode code = System.Net.HttpStatusCode.InternalServerError) + { + Response.Status = code; + Response.Reason = message; + + BodyWriter.WriteJsonObject(new { Error = message }); + } + + public void ReportClientError(string message, System.Net.HttpStatusCode code = System.Net.HttpStatusCode.BadRequest) + { + ReportServerError(message, code); + } + + public bool LongPollCheck(EventPollNotify poller, ref long id, out bool isError) + { + HttpServer.HttpInput input = String.Equals(Request.Method, "POST", StringComparison.OrdinalIgnoreCase) ? Request.Form : Request.QueryString; + if (Library.Utility.Utility.ParseBool(input["longpoll"].Value, false)) + { + long lastEventId; + if (!long.TryParse(input["lasteventid"].Value, out lastEventId)) + { + ReportClientError("When activating long poll, the request must include the last event id", System.Net.HttpStatusCode.BadRequest); + isError = true; + return false; + } + + TimeSpan ts; + try { ts = Library.Utility.Timeparser.ParseTimeSpan(input["duration"].Value); } + catch (Exception ex) + { + ReportClientError("Invalid duration: " + ex.Message, System.Net.HttpStatusCode.BadRequest); + isError = true; + return false; + } + + if (ts <= TimeSpan.FromSeconds(10) || ts.TotalMilliseconds > int.MaxValue) + { + ReportClientError("Invalid duration, must be at least 10 seconds, and less than " + int.MaxValue + " milliseconds", System.Net.HttpStatusCode.BadRequest); + isError = true; + return false; + } + + isError = false; + id = poller.Wait(lastEventId, (int)ts.TotalMilliseconds); + return true; + } + + isError = false; + return false; + } + + public void OutputOK(object item = null) + { + BodyWriter.OutputOK(item); + } + + public void OutputError(object item = null, System.Net.HttpStatusCode code = System.Net.HttpStatusCode.InternalServerError, string reason = null) + { + Response.Status = code; + Response.Reason = reason ?? "Error"; + BodyWriter.WriteJsonObject(item); + } + + public void Dispose() + { + if (BodyWriter != null) + { + var bw = BodyWriter; + BodyWriter = null; + bw.Dispose(); + } + } + } +} + diff --git a/Duplicati/Server/WebServer/RESTMethods/ServerSetting.cs b/Duplicati.Library.RestAPI/RESTMethods/ServerSetting.cs similarity index 85% rename from Duplicati/Server/WebServer/RESTMethods/ServerSetting.cs rename to Duplicati.Library.RestAPI/RESTMethods/ServerSetting.cs index 9454c15fa..026f00ee2 100644 --- a/Duplicati/Server/WebServer/RESTMethods/ServerSetting.cs +++ b/Duplicati.Library.RestAPI/RESTMethods/ServerSetting.cs @@ -1,111 +1,112 @@ -// Copyright (C) 2015, The Duplicati Team -// http://www.duplicati.com, info@duplicati.com -// -// This library is free software; you can redistribute it and/or modify -// it under the terms of the GNU Lesser General Public License as -// published by the Free Software Foundation; either version 2.1 of the -// License, or (at your option) any later version. -// -// This library is distributed in the hope that it will be useful, but -// WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU -// Lesser General Public License for more details. -// -// You should have received a copy of the GNU Lesser General Public -// License along with this library; if not, write to the Free Software -// Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA -using System; -using System.Collections.Generic; -using System.Linq; - -namespace Duplicati.Server.WebServer.RESTMethods -{ - public class ServerSetting : IRESTMethodGET, IRESTMethodPUT, IRESTMethodDocumented - { - public void GET(string key, RequestInfo info) - { - if (string.IsNullOrWhiteSpace(key)) - { - info.OutputError(null, System.Net.HttpStatusCode.BadRequest, "Key is missing"); - return; - } - - if (key.Equals("server-ssl-certificate", StringComparison.OrdinalIgnoreCase) || key.Equals("ServerSSLCertificate", StringComparison.OrdinalIgnoreCase)) - { - info.OutputOK(Program.DataConnection.ApplicationSettings.ServerSSLCertificate == null ? "False" : "True"); - return; - } - - if (key.StartsWith("--", StringComparison.Ordinal)) - { - var prop = Program.DataConnection.Settings.FirstOrDefault(x => string.Equals(key, x.Name, StringComparison.OrdinalIgnoreCase)); - info.OutputOK(prop == null ? null : prop.Value); - } - else - { - var prop = typeof(Database.ServerSettings).GetProperty(key); - if (prop == null) - info.OutputError(null, System.Net.HttpStatusCode.NotFound, "Not found"); - else - info.OutputOK(prop.GetValue(Program.DataConnection.ApplicationSettings)); - } - } - - public void PUT(string key, RequestInfo info) - { - if (string.IsNullOrWhiteSpace(key)) - { - info.OutputError(null, System.Net.HttpStatusCode.BadRequest, "Key is missing"); - return; - } - - if (key.Equals("server-ssl-certificate", StringComparison.OrdinalIgnoreCase) || key.Equals("ServerSSLCertificate", StringComparison.OrdinalIgnoreCase)) - { - info.OutputError(null, System.Net.HttpStatusCode.BadRequest, "Can only update SSL certificate from commandline"); - return; - } - - if (key.StartsWith("--", StringComparison.Ordinal)) - { - var settings = Program.DataConnection.Settings.ToList(); - - var prop = settings.FirstOrDefault(x => string.Equals(key, x.Name, StringComparison.OrdinalIgnoreCase)); - if (prop == null) - settings.Add(prop = new Database.Setting() { Name = key, Value = info.Request.Form["data"].Value }); - else - prop.Value = info.Request.Form["data"].Value; - - Program.DataConnection.Settings = settings.ToArray(); - - info.OutputOK(prop == null ? null : prop.Value); - } - else - { - var prop = typeof(Database.ServerSettings).GetProperty(key); - if (prop == null) - info.OutputError(null, System.Net.HttpStatusCode.NotFound, "Not found"); - else - { - var dict = new Dictionary(); - dict[key] = info.Request.Form["data"].Value; - Program.DataConnection.ApplicationSettings.UpdateSettings(dict, false); - info.OutputOK(); - } - } - } - - public string Description { get { return "Return a list of settings for the server"; } } - - public IEnumerable> Types - { - get - { - return new KeyValuePair[] { - new KeyValuePair(HttpServer.Method.Get, typeof(string)), - new KeyValuePair(HttpServer.Method.Put, typeof(string)) - }; - } - } - } -} - +// Copyright (C) 2015, The Duplicati Team +// http://www.duplicati.com, info@duplicati.com +// +// This library is free software; you can redistribute it and/or modify +// it under the terms of the GNU Lesser General Public License as +// published by the Free Software Foundation; either version 2.1 of the +// License, or (at your option) any later version. +// +// This library is distributed in the hope that it will be useful, but +// WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +// Lesser General Public License for more details. +// +// You should have received a copy of the GNU Lesser General Public +// License along with this library; if not, write to the Free Software +// Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA +using Duplicati.Library.RestAPI; +using System; +using System.Collections.Generic; +using System.Linq; + +namespace Duplicati.Server.WebServer.RESTMethods +{ + public class ServerSetting : IRESTMethodGET, IRESTMethodPUT, IRESTMethodDocumented + { + public void GET(string key, RequestInfo info) + { + if (string.IsNullOrWhiteSpace(key)) + { + info.OutputError(null, System.Net.HttpStatusCode.BadRequest, "Key is missing"); + return; + } + + if (key.Equals("server-ssl-certificate", StringComparison.OrdinalIgnoreCase) || key.Equals("ServerSSLCertificate", StringComparison.OrdinalIgnoreCase)) + { + info.OutputOK(FIXMEGlobal.DataConnection.ApplicationSettings.ServerSSLCertificate == null ? "False" : "True"); + return; + } + + if (key.StartsWith("--", StringComparison.Ordinal)) + { + var prop = FIXMEGlobal.DataConnection.Settings.FirstOrDefault(x => string.Equals(key, x.Name, StringComparison.OrdinalIgnoreCase)); + info.OutputOK(prop == null ? null : prop.Value); + } + else + { + var prop = typeof(Database.ServerSettings).GetProperty(key); + if (prop == null) + info.OutputError(null, System.Net.HttpStatusCode.NotFound, "Not found"); + else + info.OutputOK(prop.GetValue(FIXMEGlobal.DataConnection.ApplicationSettings)); + } + } + + public void PUT(string key, RequestInfo info) + { + if (string.IsNullOrWhiteSpace(key)) + { + info.OutputError(null, System.Net.HttpStatusCode.BadRequest, "Key is missing"); + return; + } + + if (key.Equals("server-ssl-certificate", StringComparison.OrdinalIgnoreCase) || key.Equals("ServerSSLCertificate", StringComparison.OrdinalIgnoreCase)) + { + info.OutputError(null, System.Net.HttpStatusCode.BadRequest, "Can only update SSL certificate from commandline"); + return; + } + + if (key.StartsWith("--", StringComparison.Ordinal)) + { + var settings = FIXMEGlobal.DataConnection.Settings.ToList(); + + var prop = settings.FirstOrDefault(x => string.Equals(key, x.Name, StringComparison.OrdinalIgnoreCase)); + if (prop == null) + settings.Add(prop = new Database.Setting() { Name = key, Value = info.Request.Form["data"].Value }); + else + prop.Value = info.Request.Form["data"].Value; + + FIXMEGlobal.DataConnection.Settings = settings.ToArray(); + + info.OutputOK(prop == null ? null : prop.Value); + } + else + { + var prop = typeof(Database.ServerSettings).GetProperty(key); + if (prop == null) + info.OutputError(null, System.Net.HttpStatusCode.NotFound, "Not found"); + else + { + var dict = new Dictionary(); + dict[key] = info.Request.Form["data"].Value; + FIXMEGlobal.DataConnection.ApplicationSettings.UpdateSettings(dict, false); + info.OutputOK(); + } + } + } + + public string Description { get { return "Return a list of settings for the server"; } } + + public IEnumerable> Types + { + get + { + return new KeyValuePair[] { + new KeyValuePair(HttpServer.Method.Get, typeof(string)), + new KeyValuePair(HttpServer.Method.Put, typeof(string)) + }; + } + } + } +} + diff --git a/Duplicati/Server/WebServer/RESTMethods/ServerSettings.cs b/Duplicati.Library.RestAPI/RESTMethods/ServerSettings.cs similarity index 89% rename from Duplicati/Server/WebServer/RESTMethods/ServerSettings.cs rename to Duplicati.Library.RestAPI/RESTMethods/ServerSettings.cs index cb09037d8..093e0e6d2 100644 --- a/Duplicati/Server/WebServer/RESTMethods/ServerSettings.cs +++ b/Duplicati.Library.RestAPI/RESTMethods/ServerSettings.cs @@ -1,126 +1,127 @@ -// Copyright (C) 2015, The Duplicati Team -// http://www.duplicati.com, info@duplicati.com -// -// This library is free software; you can redistribute it and/or modify -// it under the terms of the GNU Lesser General Public License as -// published by the Free Software Foundation; either version 2.1 of the -// License, or (at your option) any later version. -// -// This library is distributed in the hope that it will be useful, but -// WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU -// Lesser General Public License for more details. -// -// You should have received a copy of the GNU Lesser General Public -// License along with this library; if not, write to the Free Software -// Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA -using System; -using System.Linq; -using System.Collections.Generic; -using Duplicati.Server.Database; -using System.IO; -using Duplicati.Server.Serialization; - -namespace Duplicati.Server.WebServer.RESTMethods -{ - public class ServerSettings : IRESTMethodGET, IRESTMethodPATCH, IRESTMethodDocumented - { - public void GET(string key, RequestInfo info) - { - // Join server settings and global settings - var adv_props = - Program.DataConnection.GetSettings(Database.Connection.SERVER_SETTINGS_ID) - .Where(x => !string.IsNullOrWhiteSpace(x.Name)) - .Union( - Program.DataConnection.Settings - .Where(x => !string.IsNullOrWhiteSpace(x.Name) && x.Name.StartsWith("--", StringComparison.Ordinal)) - ); - - var dict = new Dictionary(); - foreach (var n in adv_props) - dict[n.Name] = n.Value; - - string sslcert; - dict.TryGetValue("server-ssl-certificate", out sslcert); - dict["server-ssl-certificate"] = (!string.IsNullOrWhiteSpace(sslcert)).ToString(); - - info.OutputOK(dict); - } - - public void PATCH(string key, RequestInfo info) - { - string str = info.Request.Form["data"].Value; - - if (string.IsNullOrWhiteSpace(str)) - str = new StreamReader(info.Request.Body, System.Text.Encoding.UTF8).ReadToEnd(); - - if (string.IsNullOrWhiteSpace(str)) - { - info.ReportClientError("Missing data object", System.Net.HttpStatusCode.BadRequest); - return; - } - - Dictionary data = null; - try - { - data = Serializer.Deserialize>(new StringReader(str)); - if (data == null) - { - info.ReportClientError("Data object had no entry", System.Net.HttpStatusCode.BadRequest); - return; - } - - // Split into server settings and global settings - - var serversettings = data.Where(x => !string.IsNullOrWhiteSpace(x.Key)).ToDictionary(x => x.Key, x => x.Key.StartsWith("--", StringComparison.Ordinal) ? null : x.Value); - var globalsettings = data.Where(x => !string.IsNullOrWhiteSpace(x.Key) && x.Key.StartsWith("--", StringComparison.Ordinal)); - - serversettings.Remove("server-ssl-certificate"); - serversettings.Remove("ServerSSLCertificate"); - - if (serversettings.Any()) - Program.DataConnection.ApplicationSettings.UpdateSettings(serversettings, false); - - if (globalsettings.Any()) - { - // Update based on inputs - var existing = Program.DataConnection.Settings.ToDictionary(x => x.Name, x => x); - foreach (var g in globalsettings) - if (g.Value == null) - existing.Remove(g.Key); - else - { - if (existing.ContainsKey(g.Key)) - existing[g.Key].Value = g.Value; - else - existing[g.Key] = new Setting() { Name = g.Key, Value = g.Value }; - } - - Program.DataConnection.Settings = existing.Select(x => x.Value).ToArray(); - } - - info.OutputOK(); - } - catch (Exception ex) - { - if (data == null) - info.ReportClientError(string.Format("Unable to parse data object: {0}", ex.Message), System.Net.HttpStatusCode.BadRequest); - else - info.ReportClientError(string.Format("Unable to save settings: {0}", ex.Message), System.Net.HttpStatusCode.InternalServerError); - } - } - - public string Description { get { return "Return a list of settings for the server"; } } - - public IEnumerable> Types - { - get - { - return new KeyValuePair[] { - new KeyValuePair(HttpServer.Method.Get, typeof(Database.ServerSettings)) - }; - } - } - } -} - +// Copyright (C) 2015, The Duplicati Team +// http://www.duplicati.com, info@duplicati.com +// +// This library is free software; you can redistribute it and/or modify +// it under the terms of the GNU Lesser General Public License as +// published by the Free Software Foundation; either version 2.1 of the +// License, or (at your option) any later version. +// +// This library is distributed in the hope that it will be useful, but +// WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +// Lesser General Public License for more details. +// +// You should have received a copy of the GNU Lesser General Public +// License along with this library; if not, write to the Free Software +// Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA +using System; +using System.Linq; +using System.Collections.Generic; +using Duplicati.Server.Database; +using System.IO; +using Duplicati.Server.Serialization; +using Duplicati.Library.RestAPI; + +namespace Duplicati.Server.WebServer.RESTMethods +{ + public class ServerSettings : IRESTMethodGET, IRESTMethodPATCH, IRESTMethodDocumented + { + public void GET(string key, RequestInfo info) + { + // Join server settings and global settings + var adv_props = + FIXMEGlobal.DataConnection.GetSettings(Database.Connection.SERVER_SETTINGS_ID) + .Where(x => !string.IsNullOrWhiteSpace(x.Name)) + .Union( + FIXMEGlobal.DataConnection.Settings + .Where(x => !string.IsNullOrWhiteSpace(x.Name) && x.Name.StartsWith("--", StringComparison.Ordinal)) + ); + + var dict = new Dictionary(); + foreach (var n in adv_props) + dict[n.Name] = n.Value; + + string sslcert; + dict.TryGetValue("server-ssl-certificate", out sslcert); + dict["server-ssl-certificate"] = (!string.IsNullOrWhiteSpace(sslcert)).ToString(); + + info.OutputOK(dict); + } + + public void PATCH(string key, RequestInfo info) + { + string str = info.Request.Form["data"].Value; + + if (string.IsNullOrWhiteSpace(str)) + str = new StreamReader(info.Request.Body, System.Text.Encoding.UTF8).ReadToEnd(); + + if (string.IsNullOrWhiteSpace(str)) + { + info.ReportClientError("Missing data object", System.Net.HttpStatusCode.BadRequest); + return; + } + + Dictionary data = null; + try + { + data = Serializer.Deserialize>(new StringReader(str)); + if (data == null) + { + info.ReportClientError("Data object had no entry", System.Net.HttpStatusCode.BadRequest); + return; + } + + // Split into server settings and global settings + + var serversettings = data.Where(x => !string.IsNullOrWhiteSpace(x.Key)).ToDictionary(x => x.Key, x => x.Key.StartsWith("--", StringComparison.Ordinal) ? null : x.Value); + var globalsettings = data.Where(x => !string.IsNullOrWhiteSpace(x.Key) && x.Key.StartsWith("--", StringComparison.Ordinal)); + + serversettings.Remove("server-ssl-certificate"); + serversettings.Remove("ServerSSLCertificate"); + + if (serversettings.Any()) + FIXMEGlobal.DataConnection.ApplicationSettings.UpdateSettings(serversettings, false); + + if (globalsettings.Any()) + { + // Update based on inputs + var existing = FIXMEGlobal.DataConnection.Settings.ToDictionary(x => x.Name, x => x); + foreach (var g in globalsettings) + if (g.Value == null) + existing.Remove(g.Key); + else + { + if (existing.ContainsKey(g.Key)) + existing[g.Key].Value = g.Value; + else + existing[g.Key] = new Setting() { Name = g.Key, Value = g.Value }; + } + + FIXMEGlobal.DataConnection.Settings = existing.Select(x => x.Value).ToArray(); + } + + info.OutputOK(); + } + catch (Exception ex) + { + if (data == null) + info.ReportClientError(string.Format("Unable to parse data object: {0}", ex.Message), System.Net.HttpStatusCode.BadRequest); + else + info.ReportClientError(string.Format("Unable to save settings: {0}", ex.Message), System.Net.HttpStatusCode.InternalServerError); + } + } + + public string Description { get { return "Return a list of settings for the server"; } } + + public IEnumerable> Types + { + get + { + return new KeyValuePair[] { + new KeyValuePair(HttpServer.Method.Get, typeof(Database.ServerSettings)) + }; + } + } + } +} + diff --git a/Duplicati/Server/WebServer/RESTMethods/ServerState.cs b/Duplicati.Library.RestAPI/RESTMethods/ServerState.cs similarity index 89% rename from Duplicati/Server/WebServer/RESTMethods/ServerState.cs rename to Duplicati.Library.RestAPI/RESTMethods/ServerState.cs index 89c065a72..835790aed 100644 --- a/Duplicati/Server/WebServer/RESTMethods/ServerState.cs +++ b/Duplicati.Library.RestAPI/RESTMethods/ServerState.cs @@ -1,99 +1,100 @@ -// Copyright (C) 2015, The Duplicati Team -// http://www.duplicati.com, info@duplicati.com -// -// This library is free software; you can redistribute it and/or modify -// it under the terms of the GNU Lesser General Public License as -// published by the Free Software Foundation; either version 2.1 of the -// License, or (at your option) any later version. -// -// This library is distributed in the hope that it will be useful, but -// WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU -// Lesser General Public License for more details. -// -// You should have received a copy of the GNU Lesser General Public -// License along with this library; if not, write to the Free Software -// Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA -using System; -using System.Collections.Generic; - -namespace Duplicati.Server.WebServer.RESTMethods -{ - public class ServerState : IRESTMethodGET, IRESTMethodPOST, IRESTMethodDocumented - { - public void GET(string key, RequestInfo info) - { - bool isError; - long id = 0; - long.TryParse(key, out id); - - if (info.LongPollCheck(Program.StatusEventNotifyer, ref id, out isError)) - { - //Make sure we do not report a higher number than the eventnotifier says - var st = new Serializable.ServerStatus(); - st.LastEventID = id; - info.OutputOK(st); - } - else if (!isError) - { - info.OutputOK(new Serializable.ServerStatus()); - } - } - - public void POST(string key, RequestInfo info) - { - var input = info.Request.Form; - switch ((key ?? "").ToLowerInvariant()) - { - case "pause": - if (input.Contains("duration") && !string.IsNullOrWhiteSpace(input["duration"].Value)) - { - TimeSpan ts; - try - { - ts = Library.Utility.Timeparser.ParseTimeSpan(input["duration"].Value); - } - catch (Exception ex) - { - info.ReportClientError(ex.Message, System.Net.HttpStatusCode.BadRequest); - return; - } - if (ts.TotalMilliseconds > 0) - Program.LiveControl.Pause(ts); - else - Program.LiveControl.Pause(); - } - else - { - Program.LiveControl.Pause(); - } - - info.OutputOK(); - return; - - case "resume": - Program.LiveControl.Resume(); - info.OutputOK(); - return; - - default: - info.ReportClientError("No such action", System.Net.HttpStatusCode.NotFound); - return; - } - } - - public string Description { get { return "Return the state of the server. This method can be long-polled."; } } - - public IEnumerable> Types - { - get - { - return new KeyValuePair[] { - new KeyValuePair(HttpServer.Method.Get, typeof(Serializable.ServerStatus)), - new KeyValuePair(HttpServer.Method.Post, typeof(Serializable.ServerStatus)) - }; - } - } - } -} - +// Copyright (C) 2015, The Duplicati Team +// http://www.duplicati.com, info@duplicati.com +// +// This library is free software; you can redistribute it and/or modify +// it under the terms of the GNU Lesser General Public License as +// published by the Free Software Foundation; either version 2.1 of the +// License, or (at your option) any later version. +// +// This library is distributed in the hope that it will be useful, but +// WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +// Lesser General Public License for more details. +// +// You should have received a copy of the GNU Lesser General Public +// License along with this library; if not, write to the Free Software +// Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA +using Duplicati.Library.RestAPI; +using System; +using System.Collections.Generic; + +namespace Duplicati.Server.WebServer.RESTMethods +{ + public class ServerState : IRESTMethodGET, IRESTMethodPOST, IRESTMethodDocumented + { + public void GET(string key, RequestInfo info) + { + bool isError; + long id = 0; + long.TryParse(key, out id); + + if (info.LongPollCheck(FIXMEGlobal.StatusEventNotifyer, ref id, out isError)) + { + //Make sure we do not report a higher number than the eventnotifier says + var st = new Serializable.ServerStatus(); + st.LastEventID = id; + info.OutputOK(st); + } + else if (!isError) + { + info.OutputOK(new Serializable.ServerStatus()); + } + } + + public void POST(string key, RequestInfo info) + { + var input = info.Request.Form; + switch ((key ?? "").ToLowerInvariant()) + { + case "pause": + if (input.Contains("duration") && !string.IsNullOrWhiteSpace(input["duration"].Value)) + { + TimeSpan ts; + try + { + ts = Library.Utility.Timeparser.ParseTimeSpan(input["duration"].Value); + } + catch (Exception ex) + { + info.ReportClientError(ex.Message, System.Net.HttpStatusCode.BadRequest); + return; + } + if (ts.TotalMilliseconds > 0) + FIXMEGlobal.LiveControl.Pause(ts); + else + FIXMEGlobal.LiveControl.Pause(); + } + else + { + FIXMEGlobal.LiveControl.Pause(); + } + + info.OutputOK(); + return; + + case "resume": + FIXMEGlobal.LiveControl.Resume(); + info.OutputOK(); + return; + + default: + info.ReportClientError("No such action", System.Net.HttpStatusCode.NotFound); + return; + } + } + + public string Description { get { return "Return the state of the server. This method can be long-polled."; } } + + public IEnumerable> Types + { + get + { + return new KeyValuePair[] { + new KeyValuePair(HttpServer.Method.Get, typeof(Serializable.ServerStatus)), + new KeyValuePair(HttpServer.Method.Post, typeof(Serializable.ServerStatus)) + }; + } + } + } +} + diff --git a/Duplicati/Server/WebServer/RESTMethods/SystemInfo.cs b/Duplicati.Library.RestAPI/RESTMethods/SystemInfo.cs similarity index 97% rename from Duplicati/Server/WebServer/RESTMethods/SystemInfo.cs rename to Duplicati.Library.RestAPI/RESTMethods/SystemInfo.cs index 3932e895a..e077772ca 100644 --- a/Duplicati/Server/WebServer/RESTMethods/SystemInfo.cs +++ b/Duplicati.Library.RestAPI/RESTMethods/SystemInfo.cs @@ -1,126 +1,127 @@ -// Copyright (C) 2015, The Duplicati Team -// http://www.duplicati.com, info@duplicati.com -// -// This library is free software; you can redistribute it and/or modify -// it under the terms of the GNU Lesser General Public License as -// published by the Free Software Foundation; either version 2.1 of the -// License, or (at your option) any later version. -// -// This library is distributed in the hope that it will be useful, but -// WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU -// Lesser General Public License for more details. -// -// You should have received a copy of the GNU Lesser General Public -// License along with this library; if not, write to the Free Software -// Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA -using System; -using System.Linq; -using System.Collections.Generic; -using Duplicati.Library.Interface; +// Copyright (C) 2015, The Duplicati Team +// http://www.duplicati.com, info@duplicati.com +// +// This library is free software; you can redistribute it and/or modify +// it under the terms of the GNU Lesser General Public License as +// published by the Free Software Foundation; either version 2.1 of the +// License, or (at your option) any later version. +// +// This library is distributed in the hope that it will be useful, but +// WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +// Lesser General Public License for more details. +// +// You should have received a copy of the GNU Lesser General Public +// License along with this library; if not, write to the Free Software +// Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA +using System; +using System.Linq; +using System.Collections.Generic; +using Duplicati.Library.Interface; using Duplicati.Library.Common; +using Duplicati.Library.RestAPI; + +namespace Duplicati.Server.WebServer.RESTMethods +{ + public class SystemInfo : IRESTMethodGET, IRESTMethodPOST, IRESTMethodDocumented + { + public string Description { get { return "Gets various system properties"; } } + public IEnumerable> Types + { + get + { + return new KeyValuePair[] { + new KeyValuePair(HttpServer.Method.Get, SystemData(null).GetType()) + }; + } + } + + public void GET(string key, RequestInfo info) + { + info.BodyWriter.OutputOK(SystemData(info)); + } + + public void POST(string key, RequestInfo info) + { + switch ((key ?? "").ToLowerInvariant()) + { + case "suppressdonationmessages": + Library.Main.Utility.SuppressDonationMessages = true; + info.OutputOK(); + return; + + case "showdonationmessages": + Library.Main.Utility.SuppressDonationMessages = false; + info.OutputOK(); + return; + + default: + info.ReportClientError("No such action", System.Net.HttpStatusCode.NotFound); + return; + } + } + + private static object SystemData(RequestInfo info) + { + var browserlanguage = RESTHandler.ParseDefaultRequestCulture(info) ?? System.Globalization.CultureInfo.InvariantCulture; + + return new + { + APIVersion = 1, + PasswordPlaceholder = Duplicati.Server.WebServer.Server.PASSWORD_PLACEHOLDER, + ServerVersion = System.Reflection.Assembly.GetExecutingAssembly().GetName().Version.ToString(), + ServerVersionName = Duplicati.License.VersionNumbers.Version, + ServerVersionType = Duplicati.Library.AutoUpdater.UpdaterManager.SelfVersion.ReleaseType, + StartedBy = FIXMEGlobal.Origin, + BaseVersionName = Duplicati.Library.AutoUpdater.UpdaterManager.BaseVersion.Displayname, + DefaultUpdateChannel = Duplicati.Library.AutoUpdater.AutoUpdateSettings.DefaultUpdateChannel, + DefaultUsageReportLevel = Duplicati.Library.UsageReporter.Reporter.DefaultReportLevel, + ServerTime = DateTime.Now, + OSType = Platform.IsClientPosix ? (Platform.IsClientOSX ? "OSX" : "Linux") : "Windows", + DirectorySeparator = System.IO.Path.DirectorySeparatorChar, + PathSeparator = System.IO.Path.PathSeparator, + CaseSensitiveFilesystem = Duplicati.Library.Utility.Utility.IsFSCaseSensitive, + MonoVersion = Duplicati.Library.Utility.Utility.IsMono ? Duplicati.Library.Utility.Utility.MonoVersion.ToString() : null, + MachineName = System.Environment.MachineName, + UserName = System.Environment.UserName, + NewLine = System.Environment.NewLine, + CLRVersion = System.Environment.Version.ToString(), + CLROSInfo = new + { + Platform = System.Environment.OSVersion.Platform.ToString(), + ServicePack = System.Environment.OSVersion.ServicePack, + Version = System.Environment.OSVersion.Version.ToString(), + VersionString = System.Environment.OSVersion.VersionString + }, + Options = Serializable.ServerSettings.Options, + CompressionModules = Serializable.ServerSettings.CompressionModules, + EncryptionModules = Serializable.ServerSettings.EncryptionModules, + BackendModules = Serializable.ServerSettings.BackendModules, + GenericModules = Serializable.ServerSettings.GenericModules, + WebModules = Serializable.ServerSettings.WebModules, + ConnectionModules = Serializable.ServerSettings.ConnectionModules, + ServerModules = Serializable.ServerSettings.ServerModules, + UsingAlternateUpdateURLs = Duplicati.Library.AutoUpdater.AutoUpdateSettings.UsesAlternateURLs, + LogLevels = Enum.GetNames(typeof(Duplicati.Library.Logging.LogMessageType)), + SuppressDonationMessages = Duplicati.Library.Main.Utility.SuppressDonationMessages, + SpecialFolders = from n in SpecialFolders.Nodes select new { ID = n.id, Path = n.resolvedpath }, + BrowserLocale = new + { + Code = browserlanguage.Name, + EnglishName = browserlanguage.EnglishName, + DisplayName = browserlanguage.NativeName + }, + SupportedLocales = + Library.Localization.LocalizationService.SupportedCultures + .Select(x => new { + Code = x, + EnglishName = new System.Globalization.CultureInfo(x).EnglishName, + DisplayName = new System.Globalization.CultureInfo(x).NativeName + } + ), + BrowserLocaleSupported = Library.Localization.LocalizationService.isCultureSupported(browserlanguage) + }; + } + } +} -namespace Duplicati.Server.WebServer.RESTMethods -{ - public class SystemInfo : IRESTMethodGET, IRESTMethodPOST, IRESTMethodDocumented - { - public string Description { get { return "Gets various system properties"; } } - public IEnumerable> Types - { - get - { - return new KeyValuePair[] { - new KeyValuePair(HttpServer.Method.Get, SystemData(null).GetType()) - }; - } - } - - public void GET(string key, RequestInfo info) - { - info.BodyWriter.OutputOK(SystemData(info)); - } - - public void POST(string key, RequestInfo info) - { - switch ((key ?? "").ToLowerInvariant()) - { - case "suppressdonationmessages": - Library.Main.Utility.SuppressDonationMessages = true; - info.OutputOK(); - return; - - case "showdonationmessages": - Library.Main.Utility.SuppressDonationMessages = false; - info.OutputOK(); - return; - - default: - info.ReportClientError("No such action", System.Net.HttpStatusCode.NotFound); - return; - } - } - - private static object SystemData(RequestInfo info) - { - var browserlanguage = RESTHandler.ParseDefaultRequestCulture(info) ?? System.Globalization.CultureInfo.InvariantCulture; - - return new - { - APIVersion = 1, - PasswordPlaceholder = Duplicati.Server.WebServer.Server.PASSWORD_PLACEHOLDER, - ServerVersion = System.Reflection.Assembly.GetExecutingAssembly().GetName().Version.ToString(), - ServerVersionName = Duplicati.License.VersionNumbers.Version, - ServerVersionType = Duplicati.Library.AutoUpdater.UpdaterManager.SelfVersion.ReleaseType, - StartedBy = Duplicati.Server.Program.Origin, - BaseVersionName = Duplicati.Library.AutoUpdater.UpdaterManager.BaseVersion.Displayname, - DefaultUpdateChannel = Duplicati.Library.AutoUpdater.AutoUpdateSettings.DefaultUpdateChannel, - DefaultUsageReportLevel = Duplicati.Library.UsageReporter.Reporter.DefaultReportLevel, - ServerTime = DateTime.Now, - OSType = Platform.IsClientPosix ? (Platform.IsClientOSX ? "OSX" : "Linux") : "Windows", - DirectorySeparator = System.IO.Path.DirectorySeparatorChar, - PathSeparator = System.IO.Path.PathSeparator, - CaseSensitiveFilesystem = Duplicati.Library.Utility.Utility.IsFSCaseSensitive, - MonoVersion = Duplicati.Library.Utility.Utility.IsMono ? Duplicati.Library.Utility.Utility.MonoVersion.ToString() : null, - MachineName = System.Environment.MachineName, - UserName = System.Environment.UserName, - NewLine = System.Environment.NewLine, - CLRVersion = System.Environment.Version.ToString(), - CLROSInfo = new - { - Platform = System.Environment.OSVersion.Platform.ToString(), - ServicePack = System.Environment.OSVersion.ServicePack, - Version = System.Environment.OSVersion.Version.ToString(), - VersionString = System.Environment.OSVersion.VersionString - }, - Options = Serializable.ServerSettings.Options, - CompressionModules = Serializable.ServerSettings.CompressionModules, - EncryptionModules = Serializable.ServerSettings.EncryptionModules, - BackendModules = Serializable.ServerSettings.BackendModules, - GenericModules = Serializable.ServerSettings.GenericModules, - WebModules = Serializable.ServerSettings.WebModules, - ConnectionModules = Serializable.ServerSettings.ConnectionModules, - ServerModules = Serializable.ServerSettings.ServerModules, - UsingAlternateUpdateURLs = Duplicati.Library.AutoUpdater.AutoUpdateSettings.UsesAlternateURLs, - LogLevels = Enum.GetNames(typeof(Duplicati.Library.Logging.LogMessageType)), - SuppressDonationMessages = Duplicati.Library.Main.Utility.SuppressDonationMessages, - SpecialFolders = from n in SpecialFolders.Nodes select new { ID = n.id, Path = n.resolvedpath }, - BrowserLocale = new - { - Code = browserlanguage.Name, - EnglishName = browserlanguage.EnglishName, - DisplayName = browserlanguage.NativeName - }, - SupportedLocales = - Library.Localization.LocalizationService.SupportedCultures - .Select(x => new { - Code = x, - EnglishName = new System.Globalization.CultureInfo(x).EnglishName, - DisplayName = new System.Globalization.CultureInfo(x).NativeName - } - ), - BrowserLocaleSupported = Library.Localization.LocalizationService.isCultureSupported(browserlanguage) - }; - } - } -} - diff --git a/Duplicati/Server/WebServer/RESTMethods/SystemWideSettings.cs b/Duplicati.Library.RestAPI/RESTMethods/SystemWideSettings.cs similarity index 97% rename from Duplicati/Server/WebServer/RESTMethods/SystemWideSettings.cs rename to Duplicati.Library.RestAPI/RESTMethods/SystemWideSettings.cs index 853192ce3..1a7ba3056 100644 --- a/Duplicati/Server/WebServer/RESTMethods/SystemWideSettings.cs +++ b/Duplicati.Library.RestAPI/RESTMethods/SystemWideSettings.cs @@ -1,44 +1,44 @@ -// Copyright (C) 2015, The Duplicati Team -// http://www.duplicati.com, info@duplicati.com -// -// This library is free software; you can redistribute it and/or modify -// it under the terms of the GNU Lesser General Public License as -// published by the Free Software Foundation; either version 2.1 of the -// License, or (at your option) any later version. -// -// This library is distributed in the hope that it will be useful, but -// WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU -// Lesser General Public License for more details. -// -// You should have received a copy of the GNU Lesser General Public -// License along with this library; if not, write to the Free Software -// Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA -using System; -using System.Collections.Generic; -using Duplicati.Library.Interface; - -namespace Duplicati.Server.WebServer.RESTMethods -{ - public class SystemWideSettings : IRESTMethodGET, IRESTMethodDocumented - { - public void GET(string key, RequestInfo info) - { - info.OutputOK(new Duplicati.Library.Main.Options(new Dictionary()).SupportedCommands); - } - - public string Description { get { return "Return a list of settings that can be applied to all backups on a system-wide basis"; } } - - public IEnumerable> Types - { - get - { - return new KeyValuePair[] { - new KeyValuePair(HttpServer.Method.Get, typeof(ICommandLineArgument[])) - }; - } - } - - } -} - +// Copyright (C) 2015, The Duplicati Team +// http://www.duplicati.com, info@duplicati.com +// +// This library is free software; you can redistribute it and/or modify +// it under the terms of the GNU Lesser General Public License as +// published by the Free Software Foundation; either version 2.1 of the +// License, or (at your option) any later version. +// +// This library is distributed in the hope that it will be useful, but +// WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +// Lesser General Public License for more details. +// +// You should have received a copy of the GNU Lesser General Public +// License along with this library; if not, write to the Free Software +// Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA +using System; +using System.Collections.Generic; +using Duplicati.Library.Interface; + +namespace Duplicati.Server.WebServer.RESTMethods +{ + public class SystemWideSettings : IRESTMethodGET, IRESTMethodDocumented + { + public void GET(string key, RequestInfo info) + { + info.OutputOK(new Duplicati.Library.Main.Options(new Dictionary()).SupportedCommands); + } + + public string Description { get { return "Return a list of settings that can be applied to all backups on a system-wide basis"; } } + + public IEnumerable> Types + { + get + { + return new KeyValuePair[] { + new KeyValuePair(HttpServer.Method.Get, typeof(ICommandLineArgument[])) + }; + } + } + + } +} + diff --git a/Duplicati/Server/WebServer/RESTMethods/Tags.cs b/Duplicati.Library.RestAPI/RESTMethods/Tags.cs similarity index 90% rename from Duplicati/Server/WebServer/RESTMethods/Tags.cs rename to Duplicati.Library.RestAPI/RESTMethods/Tags.cs index c2cb447c0..d5d8fd587 100644 --- a/Duplicati/Server/WebServer/RESTMethods/Tags.cs +++ b/Duplicati.Library.RestAPI/RESTMethods/Tags.cs @@ -1,54 +1,55 @@ -// Copyright (C) 2015, The Duplicati Team -// http://www.duplicati.com, info@duplicati.com -// -// This library is free software; you can redistribute it and/or modify -// it under the terms of the GNU Lesser General Public License as -// published by the Free Software Foundation; either version 2.1 of the -// License, or (at your option) any later version. -// -// This library is distributed in the hope that it will be useful, but -// WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU -// Lesser General Public License for more details. -// -// You should have received a copy of the GNU Lesser General Public -// License along with this library; if not, write to the Free Software -// Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA -using System; -using System.Collections.Generic; -using System.Globalization; -using System.Linq; - -namespace Duplicati.Server.WebServer.RESTMethods -{ - public class Tags : IRESTMethodGET, IRESTMethodDocumented - { - public void GET(string key, RequestInfo info) - { - var r = - from n in - Serializable.ServerSettings.CompressionModules - .Union(Serializable.ServerSettings.EncryptionModules) - .Union(Serializable.ServerSettings.BackendModules) - .Union(Serializable.ServerSettings.GenericModules) - select n.Key.ToLower(CultureInfo.InvariantCulture); - - // Append all known tags - r = r.Union(from n in Program.DataConnection.Backups select n.Tags into p from x in p select x.ToLower(CultureInfo.InvariantCulture)); - info.OutputOK(r); - } - - public string Description { get { return "Gets the list of tags"; } } - - public IEnumerable> Types - { - get - { - return new KeyValuePair[] { - new KeyValuePair(HttpServer.Method.Get, typeof(string[])), - }; - } - } - } -} - +// Copyright (C) 2015, The Duplicati Team +// http://www.duplicati.com, info@duplicati.com +// +// This library is free software; you can redistribute it and/or modify +// it under the terms of the GNU Lesser General Public License as +// published by the Free Software Foundation; either version 2.1 of the +// License, or (at your option) any later version. +// +// This library is distributed in the hope that it will be useful, but +// WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +// Lesser General Public License for more details. +// +// You should have received a copy of the GNU Lesser General Public +// License along with this library; if not, write to the Free Software +// Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA +using Duplicati.Library.RestAPI; +using System; +using System.Collections.Generic; +using System.Globalization; +using System.Linq; + +namespace Duplicati.Server.WebServer.RESTMethods +{ + public class Tags : IRESTMethodGET, IRESTMethodDocumented + { + public void GET(string key, RequestInfo info) + { + var r = + from n in + Serializable.ServerSettings.CompressionModules + .Union(Serializable.ServerSettings.EncryptionModules) + .Union(Serializable.ServerSettings.BackendModules) + .Union(Serializable.ServerSettings.GenericModules) + select n.Key.ToLower(CultureInfo.InvariantCulture); + + // Append all known tags + r = r.Union(from n in FIXMEGlobal.DataConnection.Backups select n.Tags into p from x in p select x.ToLower(CultureInfo.InvariantCulture)); + info.OutputOK(r); + } + + public string Description { get { return "Gets the list of tags"; } } + + public IEnumerable> Types + { + get + { + return new KeyValuePair[] { + new KeyValuePair(HttpServer.Method.Get, typeof(string[])), + }; + } + } + } +} + diff --git a/Duplicati/Server/WebServer/RESTMethods/Task.cs b/Duplicati.Library.RestAPI/RESTMethods/Task.cs similarity index 88% rename from Duplicati/Server/WebServer/RESTMethods/Task.cs rename to Duplicati.Library.RestAPI/RESTMethods/Task.cs index 756dd2e3e..24b3c24f9 100644 --- a/Duplicati/Server/WebServer/RESTMethods/Task.cs +++ b/Duplicati.Library.RestAPI/RESTMethods/Task.cs @@ -1,111 +1,112 @@ -#region Disclaimer / License -// Copyright (C) 2019, The Duplicati Team -// http://www.duplicati.com, info@duplicati.com -// -// This library is free software; you can redistribute it and/or -// modify it under the terms of the GNU Lesser General Public -// License as published by the Free Software Foundation; either -// version 2.1 of the License, or (at your option) any later version. -// -// This library is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU -// Lesser General Public License for more details. -// -// You should have received a copy of the GNU Lesser General Public -// License along with this library; if not, write to the Free Software -// Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA -// -#endregion -using System; -using System.Linq; -using System.Collections.Generic; - -namespace Duplicati.Server.WebServer.RESTMethods -{ - public class Task : IRESTMethodGET, IRESTMethodPOST - { - public void GET(string key, RequestInfo info) - { - var parts = (key ?? "").Split(new char[] { '/' }, 2); - long taskid; - if (long.TryParse(parts.FirstOrDefault(), out taskid)) - { - var task = Program.WorkThread.CurrentTask; - var tasks = Program.WorkThread.CurrentTasks; - - if (task != null && task.TaskID == taskid) - { - info.OutputOK(new { Status = "Running" }); - return; - } - - if (tasks.FirstOrDefault(x => x.TaskID == taskid) == null) - { - KeyValuePair[] matches; - lock(Program.MainLock) - matches = Program.TaskResultCache.Where(x => x.Key == taskid).ToArray(); - - if (matches.Length == 0) - info.ReportClientError("No such task found", System.Net.HttpStatusCode.NotFound); - else - info.OutputOK(new { - Status = matches[0].Value == null ? "Completed" : "Failed", - ErrorMessage = matches[0].Value == null ? null : matches[0].Value.Message, - Exception = matches[0].Value == null ? null : matches[0].Value.ToString() - }); - } - else - { - info.OutputOK(new { Status = "Waiting" }); - } - } - else - { - info.ReportClientError("Invalid request", System.Net.HttpStatusCode.BadRequest); - } - } - - public void POST(string key, RequestInfo info) - { - var parts = (key ?? "").Split(new char[] { '/' }, 2); - long taskid; - if (parts.Length == 2 && long.TryParse(parts.First(), out taskid)) - { - var task = Program.WorkThread.CurrentTask; - var tasks = Program.WorkThread.CurrentTasks; - - if (task != null) - tasks.Insert(0, task); - - task = tasks.FirstOrDefault(x => x.TaskID == taskid); - if (task == null) - { - info.ReportClientError("No such task", System.Net.HttpStatusCode.NotFound); - return; - } - - switch (parts.Last().ToLowerInvariant()) - { - case "stopaftercurrentfile": - task.Stop(allowCurrentFileToFinish: true); - info.OutputOK(); - return; - - case "stopnow": - task.Stop(allowCurrentFileToFinish: false); - info.OutputOK(); - return; - - case "abort": - task.Abort(); - info.OutputOK(); - return; - } - } - - info.ReportClientError("Invalid or missing task id", System.Net.HttpStatusCode.NotFound); - } - } -} - +#region Disclaimer / License +// Copyright (C) 2019, The Duplicati Team +// http://www.duplicati.com, info@duplicati.com +// +// This library is free software; you can redistribute it and/or +// modify it under the terms of the GNU Lesser General Public +// License as published by the Free Software Foundation; either +// version 2.1 of the License, or (at your option) any later version. +// +// This library is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +// Lesser General Public License for more details. +// +// You should have received a copy of the GNU Lesser General Public +// License along with this library; if not, write to the Free Software +// Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA +// +#endregion +using System; +using System.Linq; +using System.Collections.Generic; +using Duplicati.Library.RestAPI; + +namespace Duplicati.Server.WebServer.RESTMethods +{ + public class Task : IRESTMethodGET, IRESTMethodPOST + { + public void GET(string key, RequestInfo info) + { + var parts = (key ?? "").Split(new char[] { '/' }, 2); + long taskid; + if (long.TryParse(parts.FirstOrDefault(), out taskid)) + { + var task = FIXMEGlobal.WorkThread.CurrentTask; + var tasks = FIXMEGlobal.WorkThread.CurrentTasks; + + if (task != null && task.TaskID == taskid) + { + info.OutputOK(new { Status = "Running" }); + return; + } + + if (tasks.FirstOrDefault(x => x.TaskID == taskid) == null) + { + KeyValuePair[] matches; + lock(FIXMEGlobal.MainLock) + matches = FIXMEGlobal.TaskResultCache.Where(x => x.Key == taskid).ToArray(); + + if (matches.Length == 0) + info.ReportClientError("No such task found", System.Net.HttpStatusCode.NotFound); + else + info.OutputOK(new { + Status = matches[0].Value == null ? "Completed" : "Failed", + ErrorMessage = matches[0].Value == null ? null : matches[0].Value.Message, + Exception = matches[0].Value == null ? null : matches[0].Value.ToString() + }); + } + else + { + info.OutputOK(new { Status = "Waiting" }); + } + } + else + { + info.ReportClientError("Invalid request", System.Net.HttpStatusCode.BadRequest); + } + } + + public void POST(string key, RequestInfo info) + { + var parts = (key ?? "").Split(new char[] { '/' }, 2); + long taskid; + if (parts.Length == 2 && long.TryParse(parts.First(), out taskid)) + { + var task = FIXMEGlobal.WorkThread.CurrentTask; + var tasks = FIXMEGlobal.WorkThread.CurrentTasks; + + if (task != null) + tasks.Insert(0, task); + + task = tasks.FirstOrDefault(x => x.TaskID == taskid); + if (task == null) + { + info.ReportClientError("No such task", System.Net.HttpStatusCode.NotFound); + return; + } + + switch (parts.Last().ToLowerInvariant()) + { + case "stopaftercurrentfile": + task.Stop(allowCurrentFileToFinish: true); + info.OutputOK(); + return; + + case "stopnow": + task.Stop(allowCurrentFileToFinish: false); + info.OutputOK(); + return; + + case "abort": + task.Abort(); + info.OutputOK(); + return; + } + } + + info.ReportClientError("Invalid or missing task id", System.Net.HttpStatusCode.NotFound); + } + } +} + diff --git a/Duplicati/Server/WebServer/RESTMethods/Tasks.cs b/Duplicati.Library.RestAPI/RESTMethods/Tasks.cs similarity index 88% rename from Duplicati/Server/WebServer/RESTMethods/Tasks.cs rename to Duplicati.Library.RestAPI/RESTMethods/Tasks.cs index 0309a794a..731cec902 100644 --- a/Duplicati/Server/WebServer/RESTMethods/Tasks.cs +++ b/Duplicati.Library.RestAPI/RESTMethods/Tasks.cs @@ -1,36 +1,37 @@ -// Copyright (C) 2015, The Duplicati Team -// http://www.duplicati.com, info@duplicati.com -// -// This library is free software; you can redistribute it and/or modify -// it under the terms of the GNU Lesser General Public License as -// published by the Free Software Foundation; either version 2.1 of the -// License, or (at your option) any later version. -// -// This library is distributed in the hope that it will be useful, but -// WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU -// Lesser General Public License for more details. -// -// You should have received a copy of the GNU Lesser General Public -// License along with this library; if not, write to the Free Software -// Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA -using System; -using System.Collections.Generic; - -namespace Duplicati.Server.WebServer.RESTMethods -{ - public class Tasks : IRESTMethodGET - { - public void GET(string key, RequestInfo info) - { - var cur = Program.WorkThread.CurrentTask; - var n = Program.WorkThread.CurrentTasks; - - if (cur != null) - n.Insert(0, cur); - - info.OutputOK(n); - } - } -} - +// Copyright (C) 2015, The Duplicati Team +// http://www.duplicati.com, info@duplicati.com +// +// This library is free software; you can redistribute it and/or modify +// it under the terms of the GNU Lesser General Public License as +// published by the Free Software Foundation; either version 2.1 of the +// License, or (at your option) any later version. +// +// This library is distributed in the hope that it will be useful, but +// WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +// Lesser General Public License for more details. +// +// You should have received a copy of the GNU Lesser General Public +// License along with this library; if not, write to the Free Software +// Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA +using Duplicati.Library.RestAPI; +using System; +using System.Collections.Generic; + +namespace Duplicati.Server.WebServer.RESTMethods +{ + public class Tasks : IRESTMethodGET + { + public void GET(string key, RequestInfo info) + { + var cur = FIXMEGlobal.WorkThread.CurrentTask; + var n = FIXMEGlobal.WorkThread.CurrentTasks; + + if (cur != null) + n.Insert(0, cur); + + info.OutputOK(n); + } + } +} + diff --git a/Duplicati/Server/WebServer/RESTMethods/UISettings.cs b/Duplicati.Library.RestAPI/RESTMethods/UISettings.cs similarity index 86% rename from Duplicati/Server/WebServer/RESTMethods/UISettings.cs rename to Duplicati.Library.RestAPI/RESTMethods/UISettings.cs index 9966652c9..633ed00b6 100644 --- a/Duplicati/Server/WebServer/RESTMethods/UISettings.cs +++ b/Duplicati.Library.RestAPI/RESTMethods/UISettings.cs @@ -1,77 +1,78 @@ -// Copyright (C) 2015, The Duplicati Team -// http://www.duplicati.com, info@duplicati.com -// -// This library is free software; you can redistribute it and/or modify -// it under the terms of the GNU Lesser General Public License as -// published by the Free Software Foundation; either version 2.1 of the -// License, or (at your option) any later version. -// -// This library is distributed in the hope that it will be useful, but -// WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU -// Lesser General Public License for more details. -// -// You should have received a copy of the GNU Lesser General Public -// License along with this library; if not, write to the Free Software -// Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA -using System; -using System.Collections.Generic; -using Duplicati.Server.Serialization; -using System.IO; - -namespace Duplicati.Server.WebServer.RESTMethods -{ - public class UISettings : IRESTMethodGET, IRESTMethodPOST, IRESTMethodPATCH - { - public void GET(string key, RequestInfo info) - { - if (string.IsNullOrWhiteSpace(key)) - { - info.OutputOK(Program.DataConnection.GetUISettingsSchemes()); - } - else - { - info.OutputOK(Program.DataConnection.GetUISettings(key)); - } - } - - public void POST(string key, RequestInfo info) - { - PATCH(key, info); - } - - public void PATCH(string key, RequestInfo info) - { - if (string.IsNullOrWhiteSpace(key)) - { - info.ReportClientError("Scheme is missing", System.Net.HttpStatusCode.BadRequest); - return; - } - - IDictionary data; - try - { - data = Serializer.Deserialize>(new StreamReader(info.Request.Body)); - } - catch (Exception ex) - { - info.ReportClientError(string.Format("Unable to parse settings object: {0}", ex.Message), System.Net.HttpStatusCode.BadRequest); - return; - } - - if (data == null) - { - info.ReportClientError("Unable to parse settings object", System.Net.HttpStatusCode.BadRequest); - return; - } - - if (info.Request.Method == "POST") - Program.DataConnection.SetUISettings(key, data); - else - Program.DataConnection.UpdateUISettings(key, data); - info.OutputOK(); - } - - } -} - +// Copyright (C) 2015, The Duplicati Team +// http://www.duplicati.com, info@duplicati.com +// +// This library is free software; you can redistribute it and/or modify +// it under the terms of the GNU Lesser General Public License as +// published by the Free Software Foundation; either version 2.1 of the +// License, or (at your option) any later version. +// +// This library is distributed in the hope that it will be useful, but +// WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +// Lesser General Public License for more details. +// +// You should have received a copy of the GNU Lesser General Public +// License along with this library; if not, write to the Free Software +// Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA +using System; +using System.Collections.Generic; +using Duplicati.Server.Serialization; +using System.IO; +using Duplicati.Library.RestAPI; + +namespace Duplicati.Server.WebServer.RESTMethods +{ + public class UISettings : IRESTMethodGET, IRESTMethodPOST, IRESTMethodPATCH + { + public void GET(string key, RequestInfo info) + { + if (string.IsNullOrWhiteSpace(key)) + { + info.OutputOK(FIXMEGlobal.DataConnection.GetUISettingsSchemes()); + } + else + { + info.OutputOK(FIXMEGlobal.DataConnection.GetUISettings(key)); + } + } + + public void POST(string key, RequestInfo info) + { + PATCH(key, info); + } + + public void PATCH(string key, RequestInfo info) + { + if (string.IsNullOrWhiteSpace(key)) + { + info.ReportClientError("Scheme is missing", System.Net.HttpStatusCode.BadRequest); + return; + } + + IDictionary data; + try + { + data = Serializer.Deserialize>(new StreamReader(info.Request.Body)); + } + catch (Exception ex) + { + info.ReportClientError(string.Format("Unable to parse settings object: {0}", ex.Message), System.Net.HttpStatusCode.BadRequest); + return; + } + + if (data == null) + { + info.ReportClientError("Unable to parse settings object", System.Net.HttpStatusCode.BadRequest); + return; + } + + if (info.Request.Method == "POST") + FIXMEGlobal.DataConnection.SetUISettings(key, data); + else + FIXMEGlobal.DataConnection.UpdateUISettings(key, data); + info.OutputOK(); + } + + } +} + diff --git a/Duplicati/Server/WebServer/RESTMethods/Updates.cs b/Duplicati.Library.RestAPI/RESTMethods/Updates.cs similarity index 84% rename from Duplicati/Server/WebServer/RESTMethods/Updates.cs rename to Duplicati.Library.RestAPI/RESTMethods/Updates.cs index 8e72449a2..a8f5d3f20 100644 --- a/Duplicati/Server/WebServer/RESTMethods/Updates.cs +++ b/Duplicati.Library.RestAPI/RESTMethods/Updates.cs @@ -1,56 +1,57 @@ -// Copyright (C) 2015, The Duplicati Team -// http://www.duplicati.com, info@duplicati.com -// -// This library is free software; you can redistribute it and/or modify -// it under the terms of the GNU Lesser General Public License as -// published by the Free Software Foundation; either version 2.1 of the -// License, or (at your option) any later version. -// -// This library is distributed in the hope that it will be useful, but -// WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU -// Lesser General Public License for more details. -// -// You should have received a copy of the GNU Lesser General Public -// License along with this library; if not, write to the Free Software -// Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA -using System; - -namespace Duplicati.Server.WebServer.RESTMethods -{ - public class Updates : IRESTMethodPOST - { - public void POST(string key, RequestInfo info) - { - switch ((key ?? "").ToLowerInvariant()) - { - case "check": - Program.UpdatePoller.CheckNow(); - info.OutputOK(); - return; - - case "install": - Program.UpdatePoller.InstallUpdate(); - info.OutputOK(); - return; - - case "activate": - if (Program.WorkThread.CurrentTask != null || Program.WorkThread.CurrentTasks.Count != 0) - { - info.ReportServerError("Cannot activate update while task is running or scheduled"); - } - else - { - Program.UpdatePoller.ActivateUpdate(); - info.OutputOK(); - } - return; - - default: - info.ReportClientError("No such action", System.Net.HttpStatusCode.NotFound); - return; - } - } - } -} - +// Copyright (C) 2015, The Duplicati Team +// http://www.duplicati.com, info@duplicati.com +// +// This library is free software; you can redistribute it and/or modify +// it under the terms of the GNU Lesser General Public License as +// published by the Free Software Foundation; either version 2.1 of the +// License, or (at your option) any later version. +// +// This library is distributed in the hope that it will be useful, but +// WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +// Lesser General Public License for more details. +// +// You should have received a copy of the GNU Lesser General Public +// License along with this library; if not, write to the Free Software +// Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA +using Duplicati.Library.RestAPI; +using System; + +namespace Duplicati.Server.WebServer.RESTMethods +{ + public class Updates : IRESTMethodPOST + { + public void POST(string key, RequestInfo info) + { + switch ((key ?? "").ToLowerInvariant()) + { + case "check": + FIXMEGlobal.UpdatePoller.CheckNow(); + info.OutputOK(); + return; + + case "install": + FIXMEGlobal.UpdatePoller.InstallUpdate(); + info.OutputOK(); + return; + + case "activate": + if (FIXMEGlobal.WorkThread.CurrentTask != null || FIXMEGlobal.WorkThread.CurrentTasks.Count != 0) + { + info.ReportServerError("Cannot activate update while task is running or scheduled"); + } + else + { + FIXMEGlobal.UpdatePoller.ActivateUpdate(); + info.OutputOK(); + } + return; + + default: + info.ReportClientError("No such action", System.Net.HttpStatusCode.NotFound); + return; + } + } + } +} + diff --git a/Duplicati/Server/WebServer/RESTMethods/WebModule.cs b/Duplicati.Library.RestAPI/RESTMethods/WebModule.cs similarity index 97% rename from Duplicati/Server/WebServer/RESTMethods/WebModule.cs rename to Duplicati.Library.RestAPI/RESTMethods/WebModule.cs index bf75c7ed1..f9643eb76 100644 --- a/Duplicati/Server/WebServer/RESTMethods/WebModule.cs +++ b/Duplicati.Library.RestAPI/RESTMethods/WebModule.cs @@ -1,41 +1,41 @@ -// Copyright (C) 2015, The Duplicati Team -// http://www.duplicati.com, info@duplicati.com -// -// This library is free software; you can redistribute it and/or modify -// it under the terms of the GNU Lesser General Public License as -// published by the Free Software Foundation; either version 2.1 of the -// License, or (at your option) any later version. -// -// This library is distributed in the hope that it will be useful, but -// WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU -// Lesser General Public License for more details. -// -// You should have received a copy of the GNU Lesser General Public -// License along with this library; if not, write to the Free Software -// Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA -using System; -using System.Linq; - -namespace Duplicati.Server.WebServer.RESTMethods -{ - public class WebModule : IRESTMethodPOST - { - public void POST(string key, RequestInfo info) - { - var m = Duplicati.Library.DynamicLoader.WebLoader.Modules.FirstOrDefault(x => x.Key.Equals(key, StringComparison.OrdinalIgnoreCase)); - if (m == null) - { - info.ReportClientError(string.Format("No such command {0}", key), System.Net.HttpStatusCode.NotFound); - return; - } - - info.OutputOK(new { - Status = "OK", - Result = m.Execute(info.Request.Form.Where(x => !x.Name.Equals("command", StringComparison.OrdinalIgnoreCase) - ).ToDictionary(x => x.Name, x => x.Value)) - }); - } - } -} - +// Copyright (C) 2015, The Duplicati Team +// http://www.duplicati.com, info@duplicati.com +// +// This library is free software; you can redistribute it and/or modify +// it under the terms of the GNU Lesser General Public License as +// published by the Free Software Foundation; either version 2.1 of the +// License, or (at your option) any later version. +// +// This library is distributed in the hope that it will be useful, but +// WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +// Lesser General Public License for more details. +// +// You should have received a copy of the GNU Lesser General Public +// License along with this library; if not, write to the Free Software +// Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA +using System; +using System.Linq; + +namespace Duplicati.Server.WebServer.RESTMethods +{ + public class WebModule : IRESTMethodPOST + { + public void POST(string key, RequestInfo info) + { + var m = Duplicati.Library.DynamicLoader.WebLoader.Modules.FirstOrDefault(x => x.Key.Equals(key, StringComparison.OrdinalIgnoreCase)); + if (m == null) + { + info.ReportClientError(string.Format("No such command {0}", key), System.Net.HttpStatusCode.NotFound); + return; + } + + info.OutputOK(new { + Status = "OK", + Result = m.Execute(info.Request.Form.Where(x => !x.Name.Equals("command", StringComparison.OrdinalIgnoreCase) + ).ToDictionary(x => x.Name, x => x.Value)) + }); + } + } +} + diff --git a/Duplicati/Server/WebServer/RESTMethods/WebModules.cs b/Duplicati.Library.RestAPI/RESTMethods/WebModules.cs similarity index 97% rename from Duplicati/Server/WebServer/RESTMethods/WebModules.cs rename to Duplicati.Library.RestAPI/RESTMethods/WebModules.cs index 6205bd8b2..daa842bcf 100644 --- a/Duplicati/Server/WebServer/RESTMethods/WebModules.cs +++ b/Duplicati.Library.RestAPI/RESTMethods/WebModules.cs @@ -1,30 +1,30 @@ -// Copyright (C) 2015, The Duplicati Team -// http://www.duplicati.com, info@duplicati.com -// -// This library is free software; you can redistribute it and/or modify -// it under the terms of the GNU Lesser General Public License as -// published by the Free Software Foundation; either version 2.1 of the -// License, or (at your option) any later version. -// -// This library is distributed in the hope that it will be useful, but -// WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU -// Lesser General Public License for more details. -// -// You should have received a copy of the GNU Lesser General Public -// License along with this library; if not, write to the Free Software -// Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA -using System; -using System.Linq; - -namespace Duplicati.Server.WebServer.RESTMethods -{ - public class WebModules : IRESTMethodGET - { - public void GET(string key, RequestInfo info) - { - info.OutputOK(Duplicati.Library.DynamicLoader.WebLoader.Modules); - } - } -} - +// Copyright (C) 2015, The Duplicati Team +// http://www.duplicati.com, info@duplicati.com +// +// This library is free software; you can redistribute it and/or modify +// it under the terms of the GNU Lesser General Public License as +// published by the Free Software Foundation; either version 2.1 of the +// License, or (at your option) any later version. +// +// This library is distributed in the hope that it will be useful, but +// WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +// Lesser General Public License for more details. +// +// You should have received a copy of the GNU Lesser General Public +// License along with this library; if not, write to the Free Software +// Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA +using System; +using System.Linq; + +namespace Duplicati.Server.WebServer.RESTMethods +{ + public class WebModules : IRESTMethodGET + { + public void GET(string key, RequestInfo info) + { + info.OutputOK(Duplicati.Library.DynamicLoader.WebLoader.Modules); + } + } +} + diff --git a/Duplicati/Server/Runner.cs b/Duplicati.Library.RestAPI/Runner.cs similarity index 92% rename from Duplicati/Server/Runner.cs rename to Duplicati.Library.RestAPI/Runner.cs index 33a230381..d4a159f71 100644 --- a/Duplicati/Server/Runner.cs +++ b/Duplicati.Library.RestAPI/Runner.cs @@ -1,965 +1,966 @@ -#region Disclaimer / License -// Copyright (C) 2019, The Duplicati Team -// http://www.duplicati.com, info@duplicati.com -// -// This library is free software; you can redistribute it and/or -// modify it under the terms of the GNU Lesser General Public -// License as published by the Free Software Foundation; either -// version 2.1 of the License, or (at your option) any later version. -// -// This library is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU -// Lesser General Public License for more details. -// -// You should have received a copy of the GNU Lesser General Public -// License along with this library; if not, write to the Free Software -// Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA -// -#endregion -using System; -using System.Linq; -using System.Collections.Generic; -using Duplicati.Library.Interface; -using Duplicati.Server.Serialization; - -namespace Duplicati.Server -{ - public static class Runner - { - public interface IRunnerData : Duplicati.Server.Serialization.Interface.IQueuedTask - { - Duplicati.Server.Serialization.Interface.IBackup Backup { get; } - IDictionary ExtraOptions { get; } - string[] FilterStrings { get; } - void Stop(bool allowCurrentFileToFinish); - void Abort(); - void Pause(); - void Resume(); - void UpdateThrottleSpeed(); - void SetController(Duplicati.Library.Main.Controller controller); - } - - private class RunnerData : IRunnerData - { - private static long RunnerTaskID = 1; - - public Duplicati.Server.Serialization.DuplicatiOperation Operation { get; internal set; } - public Duplicati.Server.Serialization.Interface.IBackup Backup { get; internal set; } - public IDictionary ExtraOptions { get; internal set; } - public string[] FilterStrings { get; internal set; } - - public string BackupID { get { return Backup.ID; } } - public long TaskID { get { return m_taskID; } } - - internal Duplicati.Library.Main.Controller Controller { get; set; } - - public void SetController(Duplicati.Library.Main.Controller controller) - { - Controller = controller; - } - - public void Stop(bool allowCurrentFileToFinish) - { - var c = Controller; - if (c != null) - c.Stop(allowCurrentFileToFinish); - } - - public void Abort() - { - var c = Controller; - if (c != null) - c.Abort(); - } - - public void Pause() - { - var c = Controller; - if (c != null) - c.Pause(); - } - - public void Resume() - { - var c = Controller; - if (c != null) - c.Resume(); - } - - public long OriginalUploadSpeed { get; set; } - public long OriginalDownloadSpeed { get; set; } - - public void UpdateThrottleSpeed() - { - var controller = this.Controller; - if (controller == null) - return; - - var job_upload_throttle = this.OriginalUploadSpeed <= 0 ? long.MaxValue : this.OriginalUploadSpeed; - var job_download_throttle = this.OriginalDownloadSpeed <= 0 ? long.MaxValue : this.OriginalDownloadSpeed; - - var server_upload_throttle = long.MaxValue; - var server_download_throttle = long.MaxValue; - - try - { - if (!string.IsNullOrWhiteSpace(Program.DataConnection.ApplicationSettings.UploadSpeedLimit)) - server_upload_throttle = Duplicati.Library.Utility.Sizeparser.ParseSize(Program.DataConnection.ApplicationSettings.UploadSpeedLimit, "kb"); - } - catch { } - - try - { - if (!string.IsNullOrWhiteSpace(Program.DataConnection.ApplicationSettings.DownloadSpeedLimit)) - server_download_throttle = Duplicati.Library.Utility.Sizeparser.ParseSize(Program.DataConnection.ApplicationSettings.DownloadSpeedLimit, "kb"); - } - catch { } - - var upload_throttle = Math.Min(job_upload_throttle, server_upload_throttle); - var download_throttle = Math.Min(job_download_throttle, server_download_throttle); - - if (upload_throttle <= 0 || upload_throttle == long.MaxValue) - upload_throttle = 0; - - if (download_throttle <= 0 || download_throttle == long.MaxValue) - download_throttle = 0; - - controller.MaxUploadSpeed = upload_throttle; - controller.MaxDownloadSpeed = download_throttle; - } - - private readonly long m_taskID; - - public RunnerData() - { - m_taskID = System.Threading.Interlocked.Increment(ref RunnerTaskID); - } - } - - private class CustomRunnerTask : RunnerData - { - public readonly Action Run; - - public CustomRunnerTask(Action runner) - : base() - { - if (runner == null) - throw new ArgumentNullException(nameof(runner)); - Run = runner; - Operation = DuplicatiOperation.CustomRunner; - Backup = new Database.Backup(); - } - } - - public static IRunnerData CreateCustomTask(Action runner) - { - return new CustomRunnerTask(runner); - } - - public static IRunnerData CreateTask(Duplicati.Server.Serialization.DuplicatiOperation operation, Duplicati.Server.Serialization.Interface.IBackup backup, IDictionary extraOptions = null, string[] filterStrings = null) - { - return new RunnerData() { - Operation = operation, - Backup = backup, - ExtraOptions = extraOptions, - FilterStrings = filterStrings - }; - } - - public static IRunnerData CreateListTask(Duplicati.Server.Serialization.Interface.IBackup backup, string[] filters, bool onlyPrefix, bool allVersions, bool folderContents, DateTime time) - { - var dict = new Dictionary(); - if (onlyPrefix) - dict["list-prefix-only"] = "true"; - if (allVersions) - dict["all-versions"] = "true"; - if (time.Ticks > 0) - dict["time"] = Duplicati.Library.Utility.Utility.SerializeDateTime(time.ToUniversalTime()); - if (folderContents) - dict["list-folder-contents"] = "true"; - - return CreateTask( - DuplicatiOperation.List, - backup, - dict, - filters); - } - - public static IRunnerData CreateRestoreTask(Duplicati.Server.Serialization.Interface.IBackup backup, string[] filters, - DateTime time, string restoreTarget, bool overwrite, bool restore_permissions, - bool skip_metadata, string passphrase) - { - var dict = new Dictionary - { - ["time"] = Library.Utility.Utility.SerializeDateTime(time.ToUniversalTime()), - ["overwrite"] = overwrite? Boolean.TrueString : Boolean.FalseString, - ["restore-permissions"] = restore_permissions ? Boolean.TrueString : Boolean.FalseString, - ["skip-metadata"] = skip_metadata ? Boolean.TrueString : Boolean.FalseString, - ["allow-passphrase-change"] = Boolean.TrueString - }; - if (!string.IsNullOrWhiteSpace(restoreTarget)) - dict["restore-path"] = SpecialFolders.ExpandEnvironmentVariables(restoreTarget); - if (!(passphrase is null)) - dict["passphrase"] = passphrase; - - return CreateTask( - DuplicatiOperation.Restore, - backup, - dict, - filters); - } - private class MessageSink : Duplicati.Library.Main.IMessageSink - { - private class ProgressState : Server.Serialization.Interface.IProgressEventData - { - private readonly string m_backupID; - private readonly long m_taskID; - - internal Duplicati.Library.Main.BackendActionType m_backendAction; - internal string m_backendPath; - internal long m_backendFileSize; - internal long m_backendFileProgress; - internal long m_backendSpeed; - internal bool m_backendIsBlocking; - - internal string m_currentFilename; - internal long m_currentFilesize; - internal long m_currentFileoffset; - internal bool m_currentFilecomplete; - - internal Duplicati.Library.Main.OperationPhase m_phase; - internal float m_overallProgress; - internal long m_processedFileCount; - internal long m_processedFileSize; - internal long m_totalFileCount; - internal long m_totalFileSize; - internal bool m_stillCounting; - - public ProgressState(long taskId, string backupId) - { - m_backupID = backupId; - m_taskID = taskId; - } - - internal ProgressState Clone() - { - return (ProgressState)this.MemberwiseClone(); - } - - #region IProgressEventData implementation - public string BackupID { get { return m_backupID; } } - public long TaskID { get { return m_taskID; } } - public string BackendAction { get { return m_backendAction.ToString(); } } - public string BackendPath { get { return m_backendPath; } } - public long BackendFileSize { get { return m_backendFileSize; } } - public long BackendFileProgress { get { return m_backendFileProgress; } } - public long BackendSpeed { get { return m_backendSpeed; } } - public bool BackendIsBlocking { get { return m_backendIsBlocking; } } - public string CurrentFilename { get { return m_currentFilename; } } - public long CurrentFilesize { get { return m_currentFilesize; } } - public long CurrentFileoffset { get { return m_currentFileoffset; } } - public bool CurrentFilecomplete { get { return m_currentFilecomplete; } } - public string Phase { get { return m_phase.ToString(); } } - public float OverallProgress { get { return m_overallProgress; } } - public long ProcessedFileCount { get { return m_processedFileCount; } } - public long ProcessedFileSize { get { return m_processedFileSize; } } - public long TotalFileCount { get { return m_totalFileCount; } } - public long TotalFileSize { get { return m_totalFileSize; } } - public bool StillCounting { get { return m_stillCounting; } } - #endregion - } - - private readonly ProgressState m_state; - private Duplicati.Library.Main.IBackendProgress m_backendProgress; - private Duplicati.Library.Main.IOperationProgress m_operationProgress; - private readonly object m_lock = new object(); - - public MessageSink(long taskId, string backupId) - { - m_state = new ProgressState(taskId, backupId); - } - - public Server.Serialization.Interface.IProgressEventData Copy() - { - lock(m_lock) - { - if (m_backendProgress != null) - m_backendProgress.Update(out m_state.m_backendAction, out m_state.m_backendPath, out m_state.m_backendFileSize, out m_state.m_backendFileProgress, out m_state.m_backendSpeed, out m_state.m_backendIsBlocking); - if (m_operationProgress != null) - { - m_operationProgress.UpdateFile(out m_state.m_currentFilename, out m_state.m_currentFilesize, out m_state.m_currentFileoffset, out m_state.m_currentFilecomplete); - m_operationProgress.UpdateOverall(out m_state.m_phase, out m_state.m_overallProgress, out m_state.m_processedFileCount, out m_state.m_processedFileSize, out m_state.m_totalFileCount, out m_state.m_totalFileSize, out m_state.m_stillCounting); - } - - return m_state.Clone(); - } - } - - #region IMessageSink implementation - public void BackendEvent(Duplicati.Library.Main.BackendActionType action, Duplicati.Library.Main.BackendEventType type, string path, long size) - { - lock(m_lock) - { - m_state.m_backendAction = action; - m_state.m_backendPath = path; - if (type == Duplicati.Library.Main.BackendEventType.Started) - m_state.m_backendFileSize = size; - else if (type == Duplicati.Library.Main.BackendEventType.Progress) - m_state.m_backendFileProgress = size; - else - { - m_state.m_backendFileSize = 0; - m_state.m_backendFileProgress = 0; - m_state.m_backendSpeed = 0; - } - } - } - - public void SetBackendProgress(Library.Main.IBackendProgress progress) - { - lock (m_lock) - m_backendProgress = progress; - } - - public void SetOperationProgress(Library.Main.IOperationProgress progress) - { - lock (m_lock) - m_operationProgress = progress; - } - - public void WriteMessage(Library.Logging.LogEntry entry) - { - // Do nothing. Implementation needed for ILogDestination interface. - } - #endregion - } - - public static string GetCommandLine(IRunnerData data) - { - var backup = data.Backup; - - var options = ApplyOptions(backup, GetCommonOptions()); - if (data.ExtraOptions != null) - foreach(var k in data.ExtraOptions) - options[k.Key] = k.Value; - - var cf = Program.DataConnection.Filters; - var bf = backup.Filters; - - var sources = - (from n in backup.Sources - let p = SpecialFolders.ExpandEnvironmentVariables(n) - where !string.IsNullOrWhiteSpace(p) - select p).ToArray(); - - var exe = - System.IO.Path.Combine( - Library.AutoUpdater.UpdaterManager.InstalledBaseDir, - System.IO.Path.GetFileName( - typeof(Duplicati.CommandLine.Commands).Assembly.Location - ) - ); - - var cmd = new System.Text.StringBuilder(); - if (Library.Utility.Utility.IsMono) - cmd.Append("mono "); - - cmd.Append(Library.Utility.Utility.WrapAsCommandLine(new string[] { exe, "backup", backup.TargetURL }, false)); - - cmd.Append(" "); - cmd.Append(Library.Utility.Utility.WrapAsCommandLine(sources, true)); - - // TODO: We should check each option to see if it is a path, and allow expansion on that - foreach(var opt in options) - cmd.AppendFormat(" --{0}={1}", opt.Key, Library.Utility.Utility.WrapCommandLineElement(opt.Value, false)); - - if (cf != null) - foreach(var f in cf) - cmd.AppendFormat(" --{0}={1}", f.Include ? "include" : "exclude", Library.Utility.Utility.WrapCommandLineElement(f.Expression, true)); - - if (bf != null) - foreach(var f in bf) - cmd.AppendFormat(" --{0}={1}", f.Include ? "include" : "exclude", Library.Utility.Utility.WrapCommandLineElement(f.Expression, true)); - - return cmd.ToString(); - } - - public static string[] GetCommandLineParts(IRunnerData data) - { - var backup = data.Backup; - - var options = ApplyOptions(backup, GetCommonOptions()); - if (data.ExtraOptions != null) - foreach (var k in data.ExtraOptions) - options[k.Key] = k.Value; - - var cf = Program.DataConnection.Filters; - var bf = backup.Filters; - - var sources = - (from n in backup.Sources - let p = SpecialFolders.ExpandEnvironmentVariables(n) - where !string.IsNullOrWhiteSpace(p) - select p).ToArray(); - - var parts = new List - { - backup.TargetURL - }; - parts.AddRange(sources); - - foreach (var opt in options) - parts.Add(string.Format("--{0}={1}", opt.Key, opt.Value)); - - if (cf != null) - foreach (var f in cf) - parts.Add(string.Format("--{0}={1}", f.Include ? "include" : "exclude", f.Expression)); - - if (bf != null) - foreach (var f in bf) - parts.Add(string.Format("--{0}={1}", f.Include ? "include" : "exclude", f.Expression)); - - return parts.ToArray(); - } - - public static Duplicati.Library.Interface.IBasicResults Run(IRunnerData data, bool fromQueue) - { - if (data is CustomRunnerTask task) - { - try - { - var sink = new MessageSink(task.TaskID, null); - Program.GenerateProgressState = sink.Copy; - Program.StatusEventNotifyer.SignalNewEvent(); - - task.Run(sink); - } - catch(Exception ex) - { - Program.DataConnection.LogError(string.Empty, "Failed while executing custom task", ex); - } - - return null; - } - - var backup = data.Backup; - if (backup.Metadata == null) - { - backup.Metadata = new Dictionary(); - } - - Duplicati.Library.Utility.TempFolder tempfolder = null; - - try - { - var sink = new MessageSink(data.TaskID, backup.ID); - if (fromQueue) - { - Program.GenerateProgressState = () => sink.Copy(); - Program.StatusEventNotifyer.SignalNewEvent(); - } - - var options = ApplyOptions(backup, GetCommonOptions()); - if (data.ExtraOptions != null) - foreach(var k in data.ExtraOptions) - options[k.Key] = k.Value; - - // Pack in the system or task config for easy restore - if (data.Operation == DuplicatiOperation.Backup && options.ContainsKey("store-task-config")) - { - tempfolder = StoreTaskConfigAndGetTempFolder(data, options); - } - - // Attach a log scope that tags all messages to relay the TaskID and BackupID - using (Library.Logging.Log.StartScope(log => { - log[LogWriteHandler.LOG_EXTRA_TASKID] = data.TaskID.ToString(); - log[LogWriteHandler.LOG_EXTRA_BACKUPID] = data.BackupID; - })) - - using(tempfolder) - using(var controller = new Duplicati.Library.Main.Controller(backup.TargetURL, options, sink)) - { - try - { - if (options.ContainsKey("throttle-upload")) - ((RunnerData)data).OriginalUploadSpeed = Duplicati.Library.Utility.Sizeparser.ParseSize(options["throttle-upload"], "kb"); - } - catch { } - - try - { - if (options.ContainsKey("throttle-download")) - ((RunnerData)data).OriginalDownloadSpeed = Duplicati.Library.Utility.Sizeparser.ParseSize(options["throttle-download"], "kb"); - } - catch { } - - ((RunnerData)data).Controller = controller; - data.UpdateThrottleSpeed(); - - if (backup.Metadata.ContainsKey("LastCompactFinished")) - controller.LastCompact = Library.Utility.Utility.DeserializeDateTime(backup.Metadata["LastCompactFinished"]); - - if (backup.Metadata.ContainsKey("LastVacuumFinished")) - controller.LastVacuum = Library.Utility.Utility.DeserializeDateTime(backup.Metadata["LastVacuumFinished"]); - - switch (data.Operation) - { - case DuplicatiOperation.Backup: - { - var filter = ApplyFilter(backup, GetCommonFilter()); - var sources = - (from n in backup.Sources - let p = SpecialFolders.ExpandEnvironmentVariables(n) - where !string.IsNullOrWhiteSpace(p) - select p).ToArray(); - - var r = controller.Backup(sources, filter); - UpdateMetadata(backup, r); - return r; - } - case DuplicatiOperation.List: - { - var r = controller.List(data.FilterStrings, null); - UpdateMetadata(backup, r); - return r; - } - case DuplicatiOperation.Repair: - { - var r = controller.Repair(data.FilterStrings == null ? null : new Library.Utility.FilterExpression(data.FilterStrings)); - UpdateMetadata(backup, r); - return r; - } - case DuplicatiOperation.RepairUpdate: - { - var r = controller.UpdateDatabaseWithVersions(); - UpdateMetadata(backup, r); - return r; - } - case DuplicatiOperation.Remove: - { - var r = controller.Delete(); - UpdateMetadata(backup, r); - return r; - } - case DuplicatiOperation.Restore: - { - var r = controller.Restore(data.FilterStrings); - UpdateMetadata(backup, r); - return r; - } - case DuplicatiOperation.Verify: - { - var r = controller.Test(); - UpdateMetadata(backup, r); - return r; - } - case DuplicatiOperation.Compact: - { - var r = controller.Compact(); - UpdateMetadata(backup, r); - return r; - } - case DuplicatiOperation.CreateReport: - { - using(var tf = new Duplicati.Library.Utility.TempFile()) - { - var r = controller.CreateLogDatabase(tf); - var tempid = Program.DataConnection.RegisterTempFile("create-bug-report", r.TargetPath, DateTime.Now.AddDays(3)); - - if (string.Equals(tf, r.TargetPath, Library.Utility.Utility.ClientFilenameStringComparison)) - tf.Protected = true; - - Program.DataConnection.RegisterNotification( - NotificationType.Information, - "Bugreport ready", - "Bugreport is ready for download", - null, - null, - "bug-report:created:" + tempid, - null, - "BugreportCreatedReady", - "", - (n, a) => n - ); - - return r; - } - } - - case DuplicatiOperation.ListRemote: - { - var r = controller.ListRemote(); - UpdateMetadata(backup, r); - return r; - } - - case DuplicatiOperation.Delete: - { - if (Library.Utility.Utility.ParseBoolOption(data.ExtraOptions, "delete-remote-files")) - controller.DeleteAllRemoteFiles(); - - if (Library.Utility.Utility.ParseBoolOption(data.ExtraOptions, "delete-local-db")) - { - string dbpath; - options.TryGetValue("dbpath", out dbpath); - - if (!string.IsNullOrWhiteSpace(dbpath) && System.IO.File.Exists(dbpath)) - System.IO.File.Delete(dbpath); - } - Program.DataConnection.DeleteBackup(backup); - Program.Scheduler.Reschedule(); - return null; - } - case DuplicatiOperation.Vacuum: - { - var r = controller.Vacuum(); - UpdateMetadata(backup, r); - return r; - } - default: - //TODO: Log this - return null; - } - } - } - catch (Exception ex) - { - Program.DataConnection.LogError(data.Backup.ID, string.Format("Failed while executing \"{0}\" with id: {1}", data.Operation, data.Backup.ID), ex); - UpdateMetadataError(data.Backup, ex); - Library.UsageReporter.Reporter.Report(ex); - - if (!fromQueue) - throw; - - return null; - } - finally - { - ((RunnerData)data).Controller = null; - } - } - - private static Duplicati.Library.Utility.TempFolder StoreTaskConfigAndGetTempFolder(IRunnerData data, Dictionary options) - { - var all_tasks = string.Equals(options["store-task-config"], "all", StringComparison.OrdinalIgnoreCase) || string.Equals(options["store-task-config"], "*", StringComparison.OrdinalIgnoreCase); - var this_task = Duplicati.Library.Utility.Utility.ParseBool(options["store-task-config"], false); - - options.Remove("store-task-config"); - - Duplicati.Library.Utility.TempFolder tempfolder = null; - if (all_tasks || this_task) - { - tempfolder = new Duplicati.Library.Utility.TempFolder(); - var temppath = System.IO.Path.Combine(tempfolder, "task-setup.json"); - using (var tempfile = Duplicati.Library.Utility.TempFile.WrapExistingFile(temppath)) - { - object taskdata = null; - if (all_tasks) - taskdata = Program.DataConnection.Backups.Where(x => !x.IsTemporary).Select(x => Program.DataConnection.PrepareBackupForExport(Program.DataConnection.GetBackup(x.ID))); - else - taskdata = new[] { Program.DataConnection.PrepareBackupForExport(data.Backup) }; - - using (var fs = System.IO.File.OpenWrite(tempfile)) - using (var sw = new System.IO.StreamWriter(fs, System.Text.Encoding.UTF8)) - Serializer.SerializeJson(sw, taskdata, true); - - tempfile.Protected = true; - - options.TryGetValue("control-files", out string controlfiles); - - if (string.IsNullOrWhiteSpace(controlfiles)) - controlfiles = tempfile; - else - controlfiles += System.IO.Path.PathSeparator + tempfile; - - options["control-files"] = controlfiles; - } - } - return tempfolder; - } - - private static void UpdateMetadataError(Duplicati.Server.Serialization.Interface.IBackup backup, Exception ex) - { - backup.Metadata["LastErrorDate"] = Library.Utility.Utility.SerializeDateTime(DateTime.UtcNow); - backup.Metadata["LastErrorMessage"] = ex.Message; - - if (!backup.IsTemporary) - Program.DataConnection.SetMetadata(backup.Metadata, long.Parse(backup.ID), null); - - string messageid = null; - if (ex is UserInformationException exception) - messageid = exception.HelpID; - - System.Threading.Interlocked.Increment(ref Program.LastDataUpdateID); - Program.DataConnection.RegisterNotification( - NotificationType.Error, - backup.IsTemporary ? - "Error" : string.Format("Error while running {0}", backup.Name), - ex.Message, - ex, - backup.ID, - "backup:show-log", - null, - messageid, - null, - (n, a) => { - return a.FirstOrDefault(x => x.BackupID == backup.ID) ?? n; - } - ); - } - - private static void UpdateMetadataLastCompact(Duplicati.Server.Serialization.Interface.IBackup backup, Duplicati.Library.Interface.ICompactResults r) - { - if (r != null) - { - backup.Metadata["LastCompactDuration"] = r.Duration.ToString(); - backup.Metadata["LastCompactStarted"] = Library.Utility.Utility.SerializeDateTime(r.BeginTime.ToUniversalTime()); - backup.Metadata["LastCompactFinished"] = Library.Utility.Utility.SerializeDateTime(r.EndTime.ToUniversalTime()); - } - } - - private static void UpdateMetadataLastVacuum(Duplicati.Server.Serialization.Interface.IBackup backup, Duplicati.Library.Interface.IVacuumResults r) - { - if (r != null) - { - backup.Metadata["LastVacuumDuration"] = r.Duration.ToString(); - backup.Metadata["LastVacuumStarted"] = Library.Utility.Utility.SerializeDateTime(r.BeginTime.ToUniversalTime()); - backup.Metadata["LastVacuumFinished"] = Library.Utility.Utility.SerializeDateTime(r.EndTime.ToUniversalTime()); - } - } - - private static void UpdateMetadata(Duplicati.Server.Serialization.Interface.IBackup backup, Duplicati.Library.Interface.IParsedBackendStatistics r) - { - if (r != null) - { - backup.Metadata["LastBackupDate"] = Library.Utility.Utility.SerializeDateTime(r.LastBackupDate.ToUniversalTime()); - backup.Metadata["BackupListCount"] = r.BackupListCount.ToString(); - backup.Metadata["TotalQuotaSpace"] = r.TotalQuotaSpace.ToString(); - backup.Metadata["FreeQuotaSpace"] = r.FreeQuotaSpace.ToString(); - backup.Metadata["AssignedQuotaSpace"] = r.AssignedQuotaSpace.ToString(); - - backup.Metadata["TargetFilesSize"] = r.KnownFileSize.ToString(); - backup.Metadata["TargetFilesCount"] = r.KnownFileCount.ToString(); - backup.Metadata["TargetSizeString"] = Duplicati.Library.Utility.Utility.FormatSizeString(r.KnownFileSize); - } - } - - private static void UpdateMetadata(Duplicati.Server.Serialization.Interface.IBackup backup, Duplicati.Library.Interface.IBasicResults result) - { - if (result is IRestoreResults r1) - { - backup.Metadata["LastRestoreDuration"] = r1.Duration.ToString(); - backup.Metadata["LastRestoreStarted"] = Library.Utility.Utility.SerializeDateTime(result.BeginTime.ToUniversalTime()); - backup.Metadata["LastRestoreFinished"] = Library.Utility.Utility.SerializeDateTime(result.EndTime.ToUniversalTime()); - } - - if (result is IParsedBackendStatistics r2) - { - UpdateMetadata(backup, r2); - } - - if (result is IBackendStatsticsReporter r3) - { - if (r3.BackendStatistics is IParsedBackendStatistics statistics) - UpdateMetadata(backup, statistics); - } - - if (result is ICompactResults r4) - { - UpdateMetadataLastCompact(backup, r4); - - if (r4.VacuumResults != null) - UpdateMetadataLastVacuum(backup, r4.VacuumResults); - } - - if (result is IVacuumResults r5) - { - UpdateMetadataLastVacuum(backup, r5); - } - - if (result is IBackupResults r) - { - backup.Metadata["SourceFilesSize"] = r.SizeOfExaminedFiles.ToString(); - backup.Metadata["SourceFilesCount"] = r.ExaminedFiles.ToString(); - backup.Metadata["SourceSizeString"] = Duplicati.Library.Utility.Utility.FormatSizeString(r.SizeOfExaminedFiles); - backup.Metadata["LastBackupStarted"] = Library.Utility.Utility.SerializeDateTime(r.BeginTime.ToUniversalTime()); - backup.Metadata["LastBackupFinished"] = Library.Utility.Utility.SerializeDateTime(r.EndTime.ToUniversalTime()); - backup.Metadata["LastBackupDuration"] = r.Duration.ToString(); - - if (r.CompactResults != null) - UpdateMetadataLastCompact(backup, r.CompactResults); - - if (r.VacuumResults != null) - UpdateMetadataLastVacuum(backup, r.VacuumResults); - - if (r.FilesWithError > 0 || r.Warnings.Any() || r.Errors.Any()) - { - string message; - string titleType; - if (r.FilesWithError > 0) - { - message = $"Errors affected {r.FilesWithError} file(s)."; - titleType = "Error"; - } - else if (r.Errors.Any()) - { - message = r.Errors.Count() == 1 ? r.Errors.Single() : $"Encountered {r.Errors.Count()} errors."; - titleType = "Error"; - } - else - { - message = r.Warnings.Count() == 1 ? r.Warnings.Single() : $"Encountered {r.Warnings.Count()} warnings."; - titleType = "Warning"; - } - - Program.DataConnection.RegisterNotification( - r.FilesWithError == 0 && !r.Errors.Any() ? NotificationType.Warning : NotificationType.Error, - backup.IsTemporary ? "Warning" : $"{titleType} while running {backup.Name}", - message, - null, - backup.ID, - "backup:show-log", - null, - null, - null, - (n, a) => - { - var existing = a.FirstOrDefault(x => x.BackupID == backup.ID); - if (existing == null) - return n; - - if (existing.Type == NotificationType.Error) - return existing; - - return n; - } - ); - } - } - else if (result.ParsedResult != Library.Interface.ParsedResultType.Success) - { - var type = result.ParsedResult == Library.Interface.ParsedResultType.Warning - ? NotificationType.Warning - : NotificationType.Error; - - var title = result.ParsedResult == Library.Interface.ParsedResultType.Warning - ? (backup.IsTemporary ? - "Warning" : string.Format("Warning while running {0}", backup.Name)) - : (backup.IsTemporary ? - "Error" : string.Format("Error while running {0}", backup.Name)); - - var message = result.ParsedResult == Library.Interface.ParsedResultType.Warning - ? string.Format("Got {0} warning(s)", result.Warnings.Count()) - : string.Format("Got {0} error(s)", result.Errors.Count()); - - Program.DataConnection.RegisterNotification( - type, - title, - message, - null, - backup.ID, - "backup:show-log", - null, - null, - "backup:show-log", - (n, a) => n - ); - } - - if (!backup.IsTemporary) - Program.DataConnection.SetMetadata(backup.Metadata, long.Parse(backup.ID), null); - - System.Threading.Interlocked.Increment(ref Program.LastDataUpdateID); - Program.StatusEventNotifyer.SignalNewEvent(); - } - - private static bool TestIfOptionApplies() - { - //TODO: Implement to avoid warnings - return true; - } - - private static void DisableModule(string module, Dictionary options) - { - string disabledModules; - string enabledModules; - - if (options.TryGetValue("enable-module", out enabledModules)) - { - var emods = (enabledModules ?? "").Split(new char[] { ',' }, StringSplitOptions.RemoveEmptyEntries); - options["enable-module"] = string.Join(",", emods.Where(x => module.Equals(x, StringComparison.OrdinalIgnoreCase))); - } - - options.TryGetValue("disable-module", out disabledModules); - var mods = (disabledModules ?? "").Split(new char[] { ',' }, StringSplitOptions.RemoveEmptyEntries); - options["disable-module"] = string.Join(",", mods.Union(new string[] { module }).Distinct(StringComparer.OrdinalIgnoreCase)); - } - - internal static Dictionary ApplyOptions(Duplicati.Server.Serialization.Interface.IBackup backup, Dictionary options) - { - options["backup-name"] = backup.Name; - options["dbpath"] = backup.DBPath; - - // Apply normal options - foreach(var o in backup.Settings) - if (!o.Name.StartsWith("--", StringComparison.Ordinal) && TestIfOptionApplies()) - options[o.Name] = o.Value; - - // Apply override options - foreach(var o in backup.Settings) - if (o.Name.StartsWith("--", StringComparison.Ordinal) && TestIfOptionApplies()) - options[o.Name.Substring(2)] = o.Value; - - - // The server hangs if the module is enabled as there is no console attached - DisableModule("console-password-input", options); - - return options; - } - - private static Library.Utility.IFilter ApplyFilter(Serialization.Interface.IBackup backup, Library.Utility.IFilter filter) - { - var f2 = backup.Filters; - if (f2 != null && f2.Length > 0) - { - var nf = - (from n in f2 - let exp = - n.Expression.StartsWith("[", StringComparison.Ordinal) && n.Expression.EndsWith("]", StringComparison.Ordinal) - ? SpecialFolders.ExpandEnvironmentVariablesRegexp(n.Expression) - : SpecialFolders.ExpandEnvironmentVariables(n.Expression) - orderby n.Order - select (Library.Utility.IFilter)(new Library.Utility.FilterExpression(exp, n.Include))) - .Aggregate((a, b) => Library.Utility.FilterExpression.Combine(a, b)); - - filter = Library.Utility.FilterExpression.Combine(filter, nf); - } - - return filter; - } - - internal static Dictionary GetCommonOptions() - { - return - (from n in Program.DataConnection.Settings - where TestIfOptionApplies() - select n).ToDictionary(k => k.Name.StartsWith("--", StringComparison.Ordinal) ? k.Name.Substring(2) : k.Name, k => k.Value); - } - - private static Duplicati.Library.Utility.IFilter GetCommonFilter() - { - var filters = Program.DataConnection.Filters; - if (filters == null || filters.Length == 0) - return null; - - return - (from n in filters - orderby n.Order - let exp = Environment.ExpandEnvironmentVariables(n.Expression) - select (Duplicati.Library.Utility.IFilter)(new Duplicati.Library.Utility.FilterExpression(exp, n.Include))) - .Aggregate((a, b) => Duplicati.Library.Utility.FilterExpression.Combine(a, b)); - } - } -} - +#region Disclaimer / License +// Copyright (C) 2019, The Duplicati Team +// http://www.duplicati.com, info@duplicati.com +// +// This library is free software; you can redistribute it and/or +// modify it under the terms of the GNU Lesser General Public +// License as published by the Free Software Foundation; either +// version 2.1 of the License, or (at your option) any later version. +// +// This library is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +// Lesser General Public License for more details. +// +// You should have received a copy of the GNU Lesser General Public +// License along with this library; if not, write to the Free Software +// Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA +// +#endregion +using System; +using System.Linq; +using System.Collections.Generic; +using Duplicati.Library.Interface; +using Duplicati.Server.Serialization; +using Duplicati.Library.RestAPI; + +namespace Duplicati.Server +{ + public static class Runner + { + public interface IRunnerData : Duplicati.Server.Serialization.Interface.IQueuedTask + { + Duplicati.Server.Serialization.Interface.IBackup Backup { get; } + IDictionary ExtraOptions { get; } + string[] FilterStrings { get; } + void Stop(bool allowCurrentFileToFinish); + void Abort(); + void Pause(); + void Resume(); + void UpdateThrottleSpeed(); + void SetController(Duplicati.Library.Main.Controller controller); + } + + private class RunnerData : IRunnerData + { + private static long RunnerTaskID = 1; + + public Duplicati.Server.Serialization.DuplicatiOperation Operation { get; internal set; } + public Duplicati.Server.Serialization.Interface.IBackup Backup { get; internal set; } + public IDictionary ExtraOptions { get; internal set; } + public string[] FilterStrings { get; internal set; } + + public string BackupID { get { return Backup.ID; } } + public long TaskID { get { return m_taskID; } } + + internal Duplicati.Library.Main.Controller Controller { get; set; } + + public void SetController(Duplicati.Library.Main.Controller controller) + { + Controller = controller; + } + + public void Stop(bool allowCurrentFileToFinish) + { + var c = Controller; + if (c != null) + c.Stop(allowCurrentFileToFinish); + } + + public void Abort() + { + var c = Controller; + if (c != null) + c.Abort(); + } + + public void Pause() + { + var c = Controller; + if (c != null) + c.Pause(); + } + + public void Resume() + { + var c = Controller; + if (c != null) + c.Resume(); + } + + public long OriginalUploadSpeed { get; set; } + public long OriginalDownloadSpeed { get; set; } + + public void UpdateThrottleSpeed() + { + var controller = this.Controller; + if (controller == null) + return; + + var job_upload_throttle = this.OriginalUploadSpeed <= 0 ? long.MaxValue : this.OriginalUploadSpeed; + var job_download_throttle = this.OriginalDownloadSpeed <= 0 ? long.MaxValue : this.OriginalDownloadSpeed; + + var server_upload_throttle = long.MaxValue; + var server_download_throttle = long.MaxValue; + + try + { + if (!string.IsNullOrWhiteSpace(FIXMEGlobal.DataConnection.ApplicationSettings.UploadSpeedLimit)) + server_upload_throttle = Duplicati.Library.Utility.Sizeparser.ParseSize(FIXMEGlobal.DataConnection.ApplicationSettings.UploadSpeedLimit, "kb"); + } + catch { } + + try + { + if (!string.IsNullOrWhiteSpace(FIXMEGlobal.DataConnection.ApplicationSettings.DownloadSpeedLimit)) + server_download_throttle = Duplicati.Library.Utility.Sizeparser.ParseSize(FIXMEGlobal.DataConnection.ApplicationSettings.DownloadSpeedLimit, "kb"); + } + catch { } + + var upload_throttle = Math.Min(job_upload_throttle, server_upload_throttle); + var download_throttle = Math.Min(job_download_throttle, server_download_throttle); + + if (upload_throttle <= 0 || upload_throttle == long.MaxValue) + upload_throttle = 0; + + if (download_throttle <= 0 || download_throttle == long.MaxValue) + download_throttle = 0; + + controller.MaxUploadSpeed = upload_throttle; + controller.MaxDownloadSpeed = download_throttle; + } + + private readonly long m_taskID; + + public RunnerData() + { + m_taskID = System.Threading.Interlocked.Increment(ref RunnerTaskID); + } + } + + private class CustomRunnerTask : RunnerData + { + public readonly Action Run; + + public CustomRunnerTask(Action runner) + : base() + { + if (runner == null) + throw new ArgumentNullException(nameof(runner)); + Run = runner; + Operation = DuplicatiOperation.CustomRunner; + Backup = new Database.Backup(); + } + } + + public static IRunnerData CreateCustomTask(Action runner) + { + return new CustomRunnerTask(runner); + } + + public static IRunnerData CreateTask(Duplicati.Server.Serialization.DuplicatiOperation operation, Duplicati.Server.Serialization.Interface.IBackup backup, IDictionary extraOptions = null, string[] filterStrings = null) + { + return new RunnerData() { + Operation = operation, + Backup = backup, + ExtraOptions = extraOptions, + FilterStrings = filterStrings + }; + } + + public static IRunnerData CreateListTask(Duplicati.Server.Serialization.Interface.IBackup backup, string[] filters, bool onlyPrefix, bool allVersions, bool folderContents, DateTime time) + { + var dict = new Dictionary(); + if (onlyPrefix) + dict["list-prefix-only"] = "true"; + if (allVersions) + dict["all-versions"] = "true"; + if (time.Ticks > 0) + dict["time"] = Duplicati.Library.Utility.Utility.SerializeDateTime(time.ToUniversalTime()); + if (folderContents) + dict["list-folder-contents"] = "true"; + + return CreateTask( + DuplicatiOperation.List, + backup, + dict, + filters); + } + + public static IRunnerData CreateRestoreTask(Duplicati.Server.Serialization.Interface.IBackup backup, string[] filters, + DateTime time, string restoreTarget, bool overwrite, bool restore_permissions, + bool skip_metadata, string passphrase) + { + var dict = new Dictionary + { + ["time"] = Library.Utility.Utility.SerializeDateTime(time.ToUniversalTime()), + ["overwrite"] = overwrite? Boolean.TrueString : Boolean.FalseString, + ["restore-permissions"] = restore_permissions ? Boolean.TrueString : Boolean.FalseString, + ["skip-metadata"] = skip_metadata ? Boolean.TrueString : Boolean.FalseString, + ["allow-passphrase-change"] = Boolean.TrueString + }; + if (!string.IsNullOrWhiteSpace(restoreTarget)) + dict["restore-path"] = SpecialFolders.ExpandEnvironmentVariables(restoreTarget); + if (!(passphrase is null)) + dict["passphrase"] = passphrase; + + return CreateTask( + DuplicatiOperation.Restore, + backup, + dict, + filters); + } + private class MessageSink : Duplicati.Library.Main.IMessageSink + { + private class ProgressState : Server.Serialization.Interface.IProgressEventData + { + private readonly string m_backupID; + private readonly long m_taskID; + + internal Duplicati.Library.Main.BackendActionType m_backendAction; + internal string m_backendPath; + internal long m_backendFileSize; + internal long m_backendFileProgress; + internal long m_backendSpeed; + internal bool m_backendIsBlocking; + + internal string m_currentFilename; + internal long m_currentFilesize; + internal long m_currentFileoffset; + internal bool m_currentFilecomplete; + + internal Duplicati.Library.Main.OperationPhase m_phase; + internal float m_overallProgress; + internal long m_processedFileCount; + internal long m_processedFileSize; + internal long m_totalFileCount; + internal long m_totalFileSize; + internal bool m_stillCounting; + + public ProgressState(long taskId, string backupId) + { + m_backupID = backupId; + m_taskID = taskId; + } + + internal ProgressState Clone() + { + return (ProgressState)this.MemberwiseClone(); + } + + #region IProgressEventData implementation + public string BackupID { get { return m_backupID; } } + public long TaskID { get { return m_taskID; } } + public string BackendAction { get { return m_backendAction.ToString(); } } + public string BackendPath { get { return m_backendPath; } } + public long BackendFileSize { get { return m_backendFileSize; } } + public long BackendFileProgress { get { return m_backendFileProgress; } } + public long BackendSpeed { get { return m_backendSpeed; } } + public bool BackendIsBlocking { get { return m_backendIsBlocking; } } + public string CurrentFilename { get { return m_currentFilename; } } + public long CurrentFilesize { get { return m_currentFilesize; } } + public long CurrentFileoffset { get { return m_currentFileoffset; } } + public bool CurrentFilecomplete { get { return m_currentFilecomplete; } } + public string Phase { get { return m_phase.ToString(); } } + public float OverallProgress { get { return m_overallProgress; } } + public long ProcessedFileCount { get { return m_processedFileCount; } } + public long ProcessedFileSize { get { return m_processedFileSize; } } + public long TotalFileCount { get { return m_totalFileCount; } } + public long TotalFileSize { get { return m_totalFileSize; } } + public bool StillCounting { get { return m_stillCounting; } } + #endregion + } + + private readonly ProgressState m_state; + private Duplicati.Library.Main.IBackendProgress m_backendProgress; + private Duplicati.Library.Main.IOperationProgress m_operationProgress; + private readonly object m_lock = new object(); + + public MessageSink(long taskId, string backupId) + { + m_state = new ProgressState(taskId, backupId); + } + + public Server.Serialization.Interface.IProgressEventData Copy() + { + lock(m_lock) + { + if (m_backendProgress != null) + m_backendProgress.Update(out m_state.m_backendAction, out m_state.m_backendPath, out m_state.m_backendFileSize, out m_state.m_backendFileProgress, out m_state.m_backendSpeed, out m_state.m_backendIsBlocking); + if (m_operationProgress != null) + { + m_operationProgress.UpdateFile(out m_state.m_currentFilename, out m_state.m_currentFilesize, out m_state.m_currentFileoffset, out m_state.m_currentFilecomplete); + m_operationProgress.UpdateOverall(out m_state.m_phase, out m_state.m_overallProgress, out m_state.m_processedFileCount, out m_state.m_processedFileSize, out m_state.m_totalFileCount, out m_state.m_totalFileSize, out m_state.m_stillCounting); + } + + return m_state.Clone(); + } + } + + #region IMessageSink implementation + public void BackendEvent(Duplicati.Library.Main.BackendActionType action, Duplicati.Library.Main.BackendEventType type, string path, long size) + { + lock(m_lock) + { + m_state.m_backendAction = action; + m_state.m_backendPath = path; + if (type == Duplicati.Library.Main.BackendEventType.Started) + m_state.m_backendFileSize = size; + else if (type == Duplicati.Library.Main.BackendEventType.Progress) + m_state.m_backendFileProgress = size; + else + { + m_state.m_backendFileSize = 0; + m_state.m_backendFileProgress = 0; + m_state.m_backendSpeed = 0; + } + } + } + + public void SetBackendProgress(Library.Main.IBackendProgress progress) + { + lock (m_lock) + m_backendProgress = progress; + } + + public void SetOperationProgress(Library.Main.IOperationProgress progress) + { + lock (m_lock) + m_operationProgress = progress; + } + + public void WriteMessage(Library.Logging.LogEntry entry) + { + // Do nothing. Implementation needed for ILogDestination interface. + } + #endregion + } + + public static string GetCommandLine(IRunnerData data) + { + var backup = data.Backup; + + var options = ApplyOptions(backup, GetCommonOptions()); + if (data.ExtraOptions != null) + foreach(var k in data.ExtraOptions) + options[k.Key] = k.Value; + + var cf = FIXMEGlobal.DataConnection.Filters; + var bf = backup.Filters; + + var sources = + (from n in backup.Sources + let p = SpecialFolders.ExpandEnvironmentVariables(n) + where !string.IsNullOrWhiteSpace(p) + select p).ToArray(); + + var exe = + System.IO.Path.Combine( + Library.AutoUpdater.UpdaterManager.InstalledBaseDir, + System.IO.Path.GetFileName( + typeof(Duplicati.CommandLine.Commands).Assembly.Location + ) + ); + + var cmd = new System.Text.StringBuilder(); + if (Library.Utility.Utility.IsMono) + cmd.Append("mono "); + + cmd.Append(Library.Utility.Utility.WrapAsCommandLine(new string[] { exe, "backup", backup.TargetURL }, false)); + + cmd.Append(" "); + cmd.Append(Library.Utility.Utility.WrapAsCommandLine(sources, true)); + + // TODO: We should check each option to see if it is a path, and allow expansion on that + foreach(var opt in options) + cmd.AppendFormat(" --{0}={1}", opt.Key, Library.Utility.Utility.WrapCommandLineElement(opt.Value, false)); + + if (cf != null) + foreach(var f in cf) + cmd.AppendFormat(" --{0}={1}", f.Include ? "include" : "exclude", Library.Utility.Utility.WrapCommandLineElement(f.Expression, true)); + + if (bf != null) + foreach(var f in bf) + cmd.AppendFormat(" --{0}={1}", f.Include ? "include" : "exclude", Library.Utility.Utility.WrapCommandLineElement(f.Expression, true)); + + return cmd.ToString(); + } + + public static string[] GetCommandLineParts(IRunnerData data) + { + var backup = data.Backup; + + var options = ApplyOptions(backup, GetCommonOptions()); + if (data.ExtraOptions != null) + foreach (var k in data.ExtraOptions) + options[k.Key] = k.Value; + + var cf = FIXMEGlobal.DataConnection.Filters; + var bf = backup.Filters; + + var sources = + (from n in backup.Sources + let p = SpecialFolders.ExpandEnvironmentVariables(n) + where !string.IsNullOrWhiteSpace(p) + select p).ToArray(); + + var parts = new List + { + backup.TargetURL + }; + parts.AddRange(sources); + + foreach (var opt in options) + parts.Add(string.Format("--{0}={1}", opt.Key, opt.Value)); + + if (cf != null) + foreach (var f in cf) + parts.Add(string.Format("--{0}={1}", f.Include ? "include" : "exclude", f.Expression)); + + if (bf != null) + foreach (var f in bf) + parts.Add(string.Format("--{0}={1}", f.Include ? "include" : "exclude", f.Expression)); + + return parts.ToArray(); + } + + public static Duplicati.Library.Interface.IBasicResults Run(IRunnerData data, bool fromQueue) + { + if (data is CustomRunnerTask task) + { + try + { + var sink = new MessageSink(task.TaskID, null); + FIXMEGlobal.GenerateProgressState = sink.Copy; + FIXMEGlobal.StatusEventNotifyer.SignalNewEvent(); + + task.Run(sink); + } + catch(Exception ex) + { + FIXMEGlobal.DataConnection.LogError(string.Empty, "Failed while executing custom task", ex); + } + + return null; + } + + var backup = data.Backup; + if (backup.Metadata == null) + { + backup.Metadata = new Dictionary(); + } + + Duplicati.Library.Utility.TempFolder tempfolder = null; + + try + { + var sink = new MessageSink(data.TaskID, backup.ID); + if (fromQueue) + { + FIXMEGlobal.GenerateProgressState = () => sink.Copy(); + FIXMEGlobal.StatusEventNotifyer.SignalNewEvent(); + } + + var options = ApplyOptions(backup, GetCommonOptions()); + if (data.ExtraOptions != null) + foreach(var k in data.ExtraOptions) + options[k.Key] = k.Value; + + // Pack in the system or task config for easy restore + if (data.Operation == DuplicatiOperation.Backup && options.ContainsKey("store-task-config")) + { + tempfolder = StoreTaskConfigAndGetTempFolder(data, options); + } + + // Attach a log scope that tags all messages to relay the TaskID and BackupID + using (Library.Logging.Log.StartScope(log => { + log[LogWriteHandler.LOG_EXTRA_TASKID] = data.TaskID.ToString(); + log[LogWriteHandler.LOG_EXTRA_BACKUPID] = data.BackupID; + })) + + using(tempfolder) + using(var controller = new Duplicati.Library.Main.Controller(backup.TargetURL, options, sink)) + { + try + { + if (options.ContainsKey("throttle-upload")) + ((RunnerData)data).OriginalUploadSpeed = Duplicati.Library.Utility.Sizeparser.ParseSize(options["throttle-upload"], "kb"); + } + catch { } + + try + { + if (options.ContainsKey("throttle-download")) + ((RunnerData)data).OriginalDownloadSpeed = Duplicati.Library.Utility.Sizeparser.ParseSize(options["throttle-download"], "kb"); + } + catch { } + + ((RunnerData)data).Controller = controller; + data.UpdateThrottleSpeed(); + + if (backup.Metadata.ContainsKey("LastCompactFinished")) + controller.LastCompact = Library.Utility.Utility.DeserializeDateTime(backup.Metadata["LastCompactFinished"]); + + if (backup.Metadata.ContainsKey("LastVacuumFinished")) + controller.LastVacuum = Library.Utility.Utility.DeserializeDateTime(backup.Metadata["LastVacuumFinished"]); + + switch (data.Operation) + { + case DuplicatiOperation.Backup: + { + var filter = ApplyFilter(backup, GetCommonFilter()); + var sources = + (from n in backup.Sources + let p = SpecialFolders.ExpandEnvironmentVariables(n) + where !string.IsNullOrWhiteSpace(p) + select p).ToArray(); + + var r = controller.Backup(sources, filter); + UpdateMetadata(backup, r); + return r; + } + case DuplicatiOperation.List: + { + var r = controller.List(data.FilterStrings, null); + UpdateMetadata(backup, r); + return r; + } + case DuplicatiOperation.Repair: + { + var r = controller.Repair(data.FilterStrings == null ? null : new Library.Utility.FilterExpression(data.FilterStrings)); + UpdateMetadata(backup, r); + return r; + } + case DuplicatiOperation.RepairUpdate: + { + var r = controller.UpdateDatabaseWithVersions(); + UpdateMetadata(backup, r); + return r; + } + case DuplicatiOperation.Remove: + { + var r = controller.Delete(); + UpdateMetadata(backup, r); + return r; + } + case DuplicatiOperation.Restore: + { + var r = controller.Restore(data.FilterStrings); + UpdateMetadata(backup, r); + return r; + } + case DuplicatiOperation.Verify: + { + var r = controller.Test(); + UpdateMetadata(backup, r); + return r; + } + case DuplicatiOperation.Compact: + { + var r = controller.Compact(); + UpdateMetadata(backup, r); + return r; + } + case DuplicatiOperation.CreateReport: + { + using(var tf = new Duplicati.Library.Utility.TempFile()) + { + var r = controller.CreateLogDatabase(tf); + var tempid = FIXMEGlobal.DataConnection.RegisterTempFile("create-bug-report", r.TargetPath, DateTime.Now.AddDays(3)); + + if (string.Equals(tf, r.TargetPath, Library.Utility.Utility.ClientFilenameStringComparison)) + tf.Protected = true; + + FIXMEGlobal.DataConnection.RegisterNotification( + NotificationType.Information, + "Bugreport ready", + "Bugreport is ready for download", + null, + null, + "bug-report:created:" + tempid, + null, + "BugreportCreatedReady", + "", + (n, a) => n + ); + + return r; + } + } + + case DuplicatiOperation.ListRemote: + { + var r = controller.ListRemote(); + UpdateMetadata(backup, r); + return r; + } + + case DuplicatiOperation.Delete: + { + if (Library.Utility.Utility.ParseBoolOption(data.ExtraOptions, "delete-remote-files")) + controller.DeleteAllRemoteFiles(); + + if (Library.Utility.Utility.ParseBoolOption(data.ExtraOptions, "delete-local-db")) + { + string dbpath; + options.TryGetValue("dbpath", out dbpath); + + if (!string.IsNullOrWhiteSpace(dbpath) && System.IO.File.Exists(dbpath)) + System.IO.File.Delete(dbpath); + } + FIXMEGlobal.DataConnection.DeleteBackup(backup); + FIXMEGlobal.Scheduler.Reschedule(); + return null; + } + case DuplicatiOperation.Vacuum: + { + var r = controller.Vacuum(); + UpdateMetadata(backup, r); + return r; + } + default: + //TODO: Log this + return null; + } + } + } + catch (Exception ex) + { + FIXMEGlobal.DataConnection.LogError(data.Backup.ID, string.Format("Failed while executing \"{0}\" with id: {1}", data.Operation, data.Backup.ID), ex); + UpdateMetadataError(data.Backup, ex); + Library.UsageReporter.Reporter.Report(ex); + + if (!fromQueue) + throw; + + return null; + } + finally + { + ((RunnerData)data).Controller = null; + } + } + + private static Duplicati.Library.Utility.TempFolder StoreTaskConfigAndGetTempFolder(IRunnerData data, Dictionary options) + { + var all_tasks = string.Equals(options["store-task-config"], "all", StringComparison.OrdinalIgnoreCase) || string.Equals(options["store-task-config"], "*", StringComparison.OrdinalIgnoreCase); + var this_task = Duplicati.Library.Utility.Utility.ParseBool(options["store-task-config"], false); + + options.Remove("store-task-config"); + + Duplicati.Library.Utility.TempFolder tempfolder = null; + if (all_tasks || this_task) + { + tempfolder = new Duplicati.Library.Utility.TempFolder(); + var temppath = System.IO.Path.Combine(tempfolder, "task-setup.json"); + using (var tempfile = Duplicati.Library.Utility.TempFile.WrapExistingFile(temppath)) + { + object taskdata = null; + if (all_tasks) + taskdata = FIXMEGlobal.DataConnection.Backups.Where(x => !x.IsTemporary).Select(x => FIXMEGlobal.DataConnection.PrepareBackupForExport(FIXMEGlobal.DataConnection.GetBackup(x.ID))); + else + taskdata = new[] { FIXMEGlobal.DataConnection.PrepareBackupForExport(data.Backup) }; + + using (var fs = System.IO.File.OpenWrite(tempfile)) + using (var sw = new System.IO.StreamWriter(fs, System.Text.Encoding.UTF8)) + Serializer.SerializeJson(sw, taskdata, true); + + tempfile.Protected = true; + + options.TryGetValue("control-files", out string controlfiles); + + if (string.IsNullOrWhiteSpace(controlfiles)) + controlfiles = tempfile; + else + controlfiles += System.IO.Path.PathSeparator + tempfile; + + options["control-files"] = controlfiles; + } + } + return tempfolder; + } + + private static void UpdateMetadataError(Duplicati.Server.Serialization.Interface.IBackup backup, Exception ex) + { + backup.Metadata["LastErrorDate"] = Library.Utility.Utility.SerializeDateTime(DateTime.UtcNow); + backup.Metadata["LastErrorMessage"] = ex.Message; + + if (!backup.IsTemporary) + FIXMEGlobal.DataConnection.SetMetadata(backup.Metadata, long.Parse(backup.ID), null); + + string messageid = null; + if (ex is UserInformationException exception) + messageid = exception.HelpID; + + FIXMEGlobal.IncrementLastDataUpdateID(); + FIXMEGlobal.DataConnection.RegisterNotification( + NotificationType.Error, + backup.IsTemporary ? + "Error" : string.Format("Error while running {0}", backup.Name), + ex.Message, + ex, + backup.ID, + "backup:show-log", + null, + messageid, + null, + (n, a) => { + return a.FirstOrDefault(x => x.BackupID == backup.ID) ?? n; + } + ); + } + + private static void UpdateMetadataLastCompact(Duplicati.Server.Serialization.Interface.IBackup backup, Duplicati.Library.Interface.ICompactResults r) + { + if (r != null) + { + backup.Metadata["LastCompactDuration"] = r.Duration.ToString(); + backup.Metadata["LastCompactStarted"] = Library.Utility.Utility.SerializeDateTime(r.BeginTime.ToUniversalTime()); + backup.Metadata["LastCompactFinished"] = Library.Utility.Utility.SerializeDateTime(r.EndTime.ToUniversalTime()); + } + } + + private static void UpdateMetadataLastVacuum(Duplicati.Server.Serialization.Interface.IBackup backup, Duplicati.Library.Interface.IVacuumResults r) + { + if (r != null) + { + backup.Metadata["LastVacuumDuration"] = r.Duration.ToString(); + backup.Metadata["LastVacuumStarted"] = Library.Utility.Utility.SerializeDateTime(r.BeginTime.ToUniversalTime()); + backup.Metadata["LastVacuumFinished"] = Library.Utility.Utility.SerializeDateTime(r.EndTime.ToUniversalTime()); + } + } + + private static void UpdateMetadata(Duplicati.Server.Serialization.Interface.IBackup backup, Duplicati.Library.Interface.IParsedBackendStatistics r) + { + if (r != null) + { + backup.Metadata["LastBackupDate"] = Library.Utility.Utility.SerializeDateTime(r.LastBackupDate.ToUniversalTime()); + backup.Metadata["BackupListCount"] = r.BackupListCount.ToString(); + backup.Metadata["TotalQuotaSpace"] = r.TotalQuotaSpace.ToString(); + backup.Metadata["FreeQuotaSpace"] = r.FreeQuotaSpace.ToString(); + backup.Metadata["AssignedQuotaSpace"] = r.AssignedQuotaSpace.ToString(); + + backup.Metadata["TargetFilesSize"] = r.KnownFileSize.ToString(); + backup.Metadata["TargetFilesCount"] = r.KnownFileCount.ToString(); + backup.Metadata["TargetSizeString"] = Duplicati.Library.Utility.Utility.FormatSizeString(r.KnownFileSize); + } + } + + private static void UpdateMetadata(Duplicati.Server.Serialization.Interface.IBackup backup, Duplicati.Library.Interface.IBasicResults result) + { + if (result is IRestoreResults r1) + { + backup.Metadata["LastRestoreDuration"] = r1.Duration.ToString(); + backup.Metadata["LastRestoreStarted"] = Library.Utility.Utility.SerializeDateTime(result.BeginTime.ToUniversalTime()); + backup.Metadata["LastRestoreFinished"] = Library.Utility.Utility.SerializeDateTime(result.EndTime.ToUniversalTime()); + } + + if (result is IParsedBackendStatistics r2) + { + UpdateMetadata(backup, r2); + } + + if (result is IBackendStatsticsReporter r3) + { + if (r3.BackendStatistics is IParsedBackendStatistics statistics) + UpdateMetadata(backup, statistics); + } + + if (result is ICompactResults r4) + { + UpdateMetadataLastCompact(backup, r4); + + if (r4.VacuumResults != null) + UpdateMetadataLastVacuum(backup, r4.VacuumResults); + } + + if (result is IVacuumResults r5) + { + UpdateMetadataLastVacuum(backup, r5); + } + + if (result is IBackupResults r) + { + backup.Metadata["SourceFilesSize"] = r.SizeOfExaminedFiles.ToString(); + backup.Metadata["SourceFilesCount"] = r.ExaminedFiles.ToString(); + backup.Metadata["SourceSizeString"] = Duplicati.Library.Utility.Utility.FormatSizeString(r.SizeOfExaminedFiles); + backup.Metadata["LastBackupStarted"] = Library.Utility.Utility.SerializeDateTime(r.BeginTime.ToUniversalTime()); + backup.Metadata["LastBackupFinished"] = Library.Utility.Utility.SerializeDateTime(r.EndTime.ToUniversalTime()); + backup.Metadata["LastBackupDuration"] = r.Duration.ToString(); + + if (r.CompactResults != null) + UpdateMetadataLastCompact(backup, r.CompactResults); + + if (r.VacuumResults != null) + UpdateMetadataLastVacuum(backup, r.VacuumResults); + + if (r.FilesWithError > 0 || r.Warnings.Any() || r.Errors.Any()) + { + string message; + string titleType; + if (r.FilesWithError > 0) + { + message = $"Errors affected {r.FilesWithError} file(s)."; + titleType = "Error"; + } + else if (r.Errors.Any()) + { + message = r.Errors.Count() == 1 ? r.Errors.Single() : $"Encountered {r.Errors.Count()} errors."; + titleType = "Error"; + } + else + { + message = r.Warnings.Count() == 1 ? r.Warnings.Single() : $"Encountered {r.Warnings.Count()} warnings."; + titleType = "Warning"; + } + + FIXMEGlobal.DataConnection.RegisterNotification( + r.FilesWithError == 0 && !r.Errors.Any() ? NotificationType.Warning : NotificationType.Error, + backup.IsTemporary ? "Warning" : $"{titleType} while running {backup.Name}", + message, + null, + backup.ID, + "backup:show-log", + null, + null, + null, + (n, a) => + { + var existing = a.FirstOrDefault(x => x.BackupID == backup.ID); + if (existing == null) + return n; + + if (existing.Type == NotificationType.Error) + return existing; + + return n; + } + ); + } + } + else if (result.ParsedResult != Library.Interface.ParsedResultType.Success) + { + var type = result.ParsedResult == Library.Interface.ParsedResultType.Warning + ? NotificationType.Warning + : NotificationType.Error; + + var title = result.ParsedResult == Library.Interface.ParsedResultType.Warning + ? (backup.IsTemporary ? + "Warning" : string.Format("Warning while running {0}", backup.Name)) + : (backup.IsTemporary ? + "Error" : string.Format("Error while running {0}", backup.Name)); + + var message = result.ParsedResult == Library.Interface.ParsedResultType.Warning + ? string.Format("Got {0} warning(s)", result.Warnings.Count()) + : string.Format("Got {0} error(s)", result.Errors.Count()); + + FIXMEGlobal.DataConnection.RegisterNotification( + type, + title, + message, + null, + backup.ID, + "backup:show-log", + null, + null, + "backup:show-log", + (n, a) => n + ); + } + + if (!backup.IsTemporary) + FIXMEGlobal.DataConnection.SetMetadata(backup.Metadata, long.Parse(backup.ID), null); + + FIXMEGlobal.IncrementLastDataUpdateID(); + FIXMEGlobal.StatusEventNotifyer.SignalNewEvent(); + } + + private static bool TestIfOptionApplies() + { + //TODO: Implement to avoid warnings + return true; + } + + private static void DisableModule(string module, Dictionary options) + { + string disabledModules; + string enabledModules; + + if (options.TryGetValue("enable-module", out enabledModules)) + { + var emods = (enabledModules ?? "").Split(new char[] { ',' }, StringSplitOptions.RemoveEmptyEntries); + options["enable-module"] = string.Join(",", emods.Where(x => module.Equals(x, StringComparison.OrdinalIgnoreCase))); + } + + options.TryGetValue("disable-module", out disabledModules); + var mods = (disabledModules ?? "").Split(new char[] { ',' }, StringSplitOptions.RemoveEmptyEntries); + options["disable-module"] = string.Join(",", mods.Union(new string[] { module }).Distinct(StringComparer.OrdinalIgnoreCase)); + } + + internal static Dictionary ApplyOptions(Duplicati.Server.Serialization.Interface.IBackup backup, Dictionary options) + { + options["backup-name"] = backup.Name; + options["dbpath"] = backup.DBPath; + + // Apply normal options + foreach(var o in backup.Settings) + if (!o.Name.StartsWith("--", StringComparison.Ordinal) && TestIfOptionApplies()) + options[o.Name] = o.Value; + + // Apply override options + foreach(var o in backup.Settings) + if (o.Name.StartsWith("--", StringComparison.Ordinal) && TestIfOptionApplies()) + options[o.Name.Substring(2)] = o.Value; + + + // The server hangs if the module is enabled as there is no console attached + DisableModule("console-password-input", options); + + return options; + } + + private static Library.Utility.IFilter ApplyFilter(Serialization.Interface.IBackup backup, Library.Utility.IFilter filter) + { + var f2 = backup.Filters; + if (f2 != null && f2.Length > 0) + { + var nf = + (from n in f2 + let exp = + n.Expression.StartsWith("[", StringComparison.Ordinal) && n.Expression.EndsWith("]", StringComparison.Ordinal) + ? SpecialFolders.ExpandEnvironmentVariablesRegexp(n.Expression) + : SpecialFolders.ExpandEnvironmentVariables(n.Expression) + orderby n.Order + select (Library.Utility.IFilter)(new Library.Utility.FilterExpression(exp, n.Include))) + .Aggregate((a, b) => Library.Utility.FilterExpression.Combine(a, b)); + + filter = Library.Utility.FilterExpression.Combine(filter, nf); + } + + return filter; + } + + internal static Dictionary GetCommonOptions() + { + return + (from n in FIXMEGlobal.DataConnection.Settings + where TestIfOptionApplies() + select n).ToDictionary(k => k.Name.StartsWith("--", StringComparison.Ordinal) ? k.Name.Substring(2) : k.Name, k => k.Value); + } + + private static Duplicati.Library.Utility.IFilter GetCommonFilter() + { + var filters = FIXMEGlobal.DataConnection.Filters; + if (filters == null || filters.Length == 0) + return null; + + return + (from n in filters + orderby n.Order + let exp = Environment.ExpandEnvironmentVariables(n.Expression) + select (Duplicati.Library.Utility.IFilter)(new Duplicati.Library.Utility.FilterExpression(exp, n.Include))) + .Aggregate((a, b) => Duplicati.Library.Utility.FilterExpression.Combine(a, b)); + } + } +} + diff --git a/Duplicati/Server/Scheduler.cs b/Duplicati.Library.RestAPI/Scheduler.cs similarity index 94% rename from Duplicati/Server/Scheduler.cs rename to Duplicati.Library.RestAPI/Scheduler.cs index c5704d1a6..d680a9a61 100644 --- a/Duplicati/Server/Scheduler.cs +++ b/Duplicati.Library.RestAPI/Scheduler.cs @@ -1,409 +1,410 @@ -#region Disclaimer / License -// Copyright (C) 2015, The Duplicati Team -// http://www.duplicati.com, info@duplicati.com -// -// This library is free software; you can redistribute it and/or -// modify it under the terms of the GNU Lesser General Public -// License as published by the Free Software Foundation; either -// version 2.1 of the License, or (at your option) any later version. -// -// This library is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU -// Lesser General Public License for more details. -// -// You should have received a copy of the GNU Lesser General Public -// License along with this library; if not, write to the Free Software -// Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA -// -using Duplicati.Server.Serialization.Interface; - - -#endregion -using System; -using System.Collections.Generic; -using System.Text; -using System.Linq; -using System.Threading; -using Duplicati.Library.Utility; - -namespace Duplicati.Server -{ - /// - /// This class handles scheduled runs of backups - /// - public class Scheduler - { - private static readonly string LOGTAG = Duplicati.Library.Logging.Log.LogTagFromType(); - - /// - /// The thread that runs the scheduler - /// - private readonly Thread m_thread; - /// - /// A termination flag - /// - private volatile bool m_terminate; - /// - /// The worker thread that is invoked to do work - /// - private readonly WorkerThread m_worker; - /// - /// The wait event - /// - private readonly AutoResetEvent m_event; - /// - /// The data synchronization lock - /// - private readonly object m_lock = new object(); - - /// - /// An event that is raised when the schedule changes - /// - public event EventHandler NewSchedule; - - /// - /// The currently scheduled items - /// - private KeyValuePair[] m_schedule; - - /// - /// List of update tasks, used to set the timestamp on the schedule once completed - /// - private readonly Dictionary> m_updateTasks; - - /// - /// Constructs a new scheduler - /// - /// The worker thread - public Scheduler(WorkerThread worker) - { - m_thread = new Thread(new ThreadStart(Runner)); - m_worker = worker; - m_worker.CompletedWork += OnCompleted; - m_worker.StartingWork += OnStartingWork; - m_schedule = new KeyValuePair[0]; - m_terminate = false; - m_event = new AutoResetEvent(false); - m_updateTasks = new Dictionary>(); - m_thread.IsBackground = true; - m_thread.Name = "TaskScheduler"; - m_thread.Start(); - } - - /// - /// Forces the scheduler to re-evaluate the order. - /// Call this method if something changes - /// - public void Reschedule() - { - m_event.Set(); - } - - /// - /// A snapshot copy of the current schedule list - /// - public List> Schedule - { - get - { - lock (m_lock) - return m_schedule.ToList(); - } - } - - /// - /// A snapshot copy of the current worker queue, that is items that are scheduled, but waiting for execution - /// - public List WorkerQueue - { - get - { - return (from t in m_worker.CurrentTasks where t != null select t).ToList(); - } - } - - /// - /// Terminates the thread. Any items still in queue will be removed - /// - /// True if the call should block until the thread has exited, false otherwise - public void Terminate(bool wait) - { - m_terminate = true; - m_event.Set(); - - if (wait) - m_thread.Join(); - } - - /// - /// Returns the next valid date, given the start and the interval - /// - /// The base time - /// The first allowed date - /// The repetition interval - /// The days the backup is allowed to run - /// The next valid date, or throws an exception if no such date can be found - public static DateTime GetNextValidTime(DateTime basetime, DateTime firstdate, string repetition, DayOfWeek[] allowedDays) - { - var res = basetime; - - var i = 50000; - while (res < firstdate && i-- > 0) - res = Timeparser.ParseTimeInterval(repetition, res); - - // If we arrived somewhere after the first allowed date - if (res >= firstdate) - { - var ts = Timeparser.ParseTimeSpan(repetition); - - if (ts.TotalDays >= 1) - { - // We jump in days, so we pick the first valid day after firstdate - - for (var n = 0; n < 8; n++) - if (IsDateAllowed(res, allowedDays)) - break; - else - res = res.AddDays(1); - } - else - { - // We jump less than a day, so we keep adding the repetition until - // we hit a valid day - - i = 50000; - while (!IsDateAllowed(res, allowedDays) && i-- > 0) - res = Timeparser.ParseTimeInterval(repetition, res); - } - } - - if (!IsDateAllowed(res, allowedDays) || res < firstdate) - { - StringBuilder sb = new StringBuilder(); - if (allowedDays != null) - foreach (DayOfWeek w in allowedDays) - { - if (sb.Length != 0) - sb.Append(", "); - sb.Append(w.ToString()); - } - - throw new Exception(Strings.Scheduler.InvalidTimeSetupError(basetime, repetition, sb.ToString())); - } - - return res; - } - - private void OnCompleted(WorkerThread worker, Runner.IRunnerData task) - { - Tuple t = null; - lock(m_lock) - { - if (task != null && m_updateTasks.TryGetValue(task, out t)) - m_updateTasks.Remove(task); - } - - if (t != null) - { - t.Item1.Time = t.Item2; - t.Item1.LastRun = t.Item3; - Program.DataConnection.AddOrUpdateSchedule(t.Item1); - } - - } - - private void OnStartingWork(WorkerThread worker, Runner.IRunnerData task) - { - if (task is null) - { - return; - } - - lock(m_lock) - { - if (m_updateTasks.TryGetValue(task, out Tuple scheduleInfo)) - { - // Item2 is the scheduled start time (Time in the Schedule table). - // Item3 is the actual start time (LastRun in the Schedule table). - m_updateTasks[task] = Tuple.Create(scheduleInfo.Item1, scheduleInfo.Item2, DateTime.UtcNow); - } - } - } - - /// - /// The actual scheduling procedure - /// - private void Runner() - { - var scheduled = new Dictionary>(); - while (!m_terminate) - { - //TODO: As this is executed repeatedly we should cache it - // to avoid frequent db lookups - - //Determine schedule list - var lst = Program.DataConnection.Schedules; - foreach(var sc in lst) - { - if (!string.IsNullOrEmpty(sc.Repeat)) - { - KeyValuePair startkey; - - DateTime last = new DateTime(0, DateTimeKind.Utc); - DateTime start; - var scticks = sc.Time.Ticks; - - if (!scheduled.TryGetValue(sc.ID, out startkey) || startkey.Key != scticks) - { - start = new DateTime(scticks, DateTimeKind.Utc); - last = sc.LastRun; - } - else - { - start = startkey.Value; - } - - try - { - // Recover from timedrift issues by overriding the dates if the last run date is in the future. - if (last > DateTime.UtcNow) - { - start = DateTime.UtcNow; - last = DateTime.UtcNow; - } - start = GetNextValidTime(start, last, sc.Repeat, sc.AllowedDays); - } - catch (Exception ex) - { - Program.DataConnection.LogError(sc.ID.ToString(), "Scheduler failed to find next date", ex); - } - - //If time is exceeded, run it now - if (start <= DateTime.UtcNow) - { - var jobsToRun = new List(); - //TODO: Cache this to avoid frequent lookups - foreach(var id in Program.DataConnection.GetBackupIDsForTags(sc.Tags).Distinct().Select(x => x.ToString())) - { - //See if it is already queued - var tmplst = from n in m_worker.CurrentTasks - where n.Operation == Duplicati.Server.Serialization.DuplicatiOperation.Backup - select n.Backup; - var tastTemp = m_worker.CurrentTask; - if (tastTemp != null && tastTemp.Operation == Duplicati.Server.Serialization.DuplicatiOperation.Backup) - tmplst = tmplst.Union(new [] { tastTemp.Backup }); - - //If it is not already in queue, put it there - if (!tmplst.Any(x => x.ID == id)) - { - var entry = Program.DataConnection.GetBackup(id); - if (entry != null) - { - Dictionary options = Duplicati.Server.Runner.GetCommonOptions(); - Duplicati.Server.Runner.ApplyOptions(entry, options); - if ((new Duplicati.Library.Main.Options(options)).DisableOnBattery && (Duplicati.Library.Utility.Power.PowerSupply.GetSource() == Duplicati.Library.Utility.Power.PowerSupply.Source.Battery)) - { - Duplicati.Library.Logging.Log.WriteInformationMessage(LOGTAG, "BackupDisabledOnBattery", "Scheduled backup disabled while on battery power."); - } - else - { - jobsToRun.Add(Server.Runner.CreateTask(Duplicati.Server.Serialization.DuplicatiOperation.Backup, entry)); - } - } - } - } - - // Calculate next time, by finding the first entry later than now - try - { - start = GetNextValidTime(start, new DateTime(Math.Max(DateTime.UtcNow.AddSeconds(1).Ticks, start.AddSeconds(1).Ticks), DateTimeKind.Utc), sc.Repeat, sc.AllowedDays); - } - catch(Exception ex) - { - Program.DataConnection.LogError(sc.ID.ToString(), "Scheduler failed to find next date", ex); - continue; - } - - Server.Runner.IRunnerData lastJob = jobsToRun.LastOrDefault(); - if (lastJob != null) - { - lock (m_lock) - { - // The actual last run time will be updated when the StartingWork event is raised. - m_updateTasks[lastJob] = new Tuple(sc, start, DateTime.UtcNow); - } - } - - foreach (var job in jobsToRun) - m_worker.AddTask(job); - - if (start < DateTime.UtcNow) - { - //TODO: Report this somehow - continue; - } - } - - scheduled[sc.ID] = new KeyValuePair(scticks, start); - } - } - - var existing = lst.ToDictionary(x => x.ID); - //Sort them, lock as we assign the m_schedule variable - lock(m_lock) - m_schedule = (from n in scheduled - where existing.ContainsKey(n.Key) - orderby n.Value.Value - select new KeyValuePair(n.Value.Value, existing[n.Key])).ToArray(); - - // Remove unused entries - foreach(var c in (from n in scheduled where !existing.ContainsKey(n.Key) select n.Key).ToArray()) - scheduled.Remove(c); - - //Raise event if needed - if (NewSchedule != null) - NewSchedule(this, null); - - int waittime = 0; - - //Figure out a sensible amount of time to sleep the thread - if (scheduled.Count > 0) - { - //When is the next run scheduled? - TimeSpan nextrun = scheduled.Values.Min((x) => x.Value) - DateTime.UtcNow; - if (nextrun.TotalMilliseconds < 0) - continue; - - //Don't sleep for more than 5 minutes - waittime = (int)Math.Min(nextrun.TotalMilliseconds, 60 * 1000 * 5); - } - else - { - //No tasks, check back later - waittime = 60 * 1000; - } - - //Waiting on the event, enables a wakeup call from termination - // never use waittime = 0 - m_event.WaitOne(Math.Max(100, waittime), false); - } - } - - /// - /// Returns true if the time is at an allowed weekday, false otherwise - /// - /// The time to evaluate - /// The allowed days - /// True if the backup is allowed to run, false otherwise - private static bool IsDateAllowed(DateTime time, DayOfWeek[] allowedDays) - { - var localTime = time.ToLocalTime(); - if (allowedDays == null || allowedDays.Length == 0) - return true; - else - return Array.IndexOf(allowedDays, localTime.DayOfWeek) >= 0; - } - - } -} +#region Disclaimer / License +// Copyright (C) 2015, The Duplicati Team +// http://www.duplicati.com, info@duplicati.com +// +// This library is free software; you can redistribute it and/or +// modify it under the terms of the GNU Lesser General Public +// License as published by the Free Software Foundation; either +// version 2.1 of the License, or (at your option) any later version. +// +// This library is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +// Lesser General Public License for more details. +// +// You should have received a copy of the GNU Lesser General Public +// License along with this library; if not, write to the Free Software +// Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA +// +using Duplicati.Server.Serialization.Interface; + + +#endregion +using System; +using System.Collections.Generic; +using System.Text; +using System.Linq; +using System.Threading; +using Duplicati.Library.Utility; +using Duplicati.Library.RestAPI; + +namespace Duplicati.Server +{ + /// + /// This class handles scheduled runs of backups + /// + public class Scheduler + { + private static readonly string LOGTAG = Duplicati.Library.Logging.Log.LogTagFromType(); + + /// + /// The thread that runs the scheduler + /// + private readonly Thread m_thread; + /// + /// A termination flag + /// + private volatile bool m_terminate; + /// + /// The worker thread that is invoked to do work + /// + private readonly WorkerThread m_worker; + /// + /// The wait event + /// + private readonly AutoResetEvent m_event; + /// + /// The data synchronization lock + /// + private readonly object m_lock = new object(); + + /// + /// An event that is raised when the schedule changes + /// + public event EventHandler NewSchedule; + + /// + /// The currently scheduled items + /// + private KeyValuePair[] m_schedule; + + /// + /// List of update tasks, used to set the timestamp on the schedule once completed + /// + private readonly Dictionary> m_updateTasks; + + /// + /// Constructs a new scheduler + /// + /// The worker thread + public Scheduler(WorkerThread worker) + { + m_thread = new Thread(new ThreadStart(Runner)); + m_worker = worker; + m_worker.CompletedWork += OnCompleted; + m_worker.StartingWork += OnStartingWork; + m_schedule = new KeyValuePair[0]; + m_terminate = false; + m_event = new AutoResetEvent(false); + m_updateTasks = new Dictionary>(); + m_thread.IsBackground = true; + m_thread.Name = "TaskScheduler"; + m_thread.Start(); + } + + /// + /// Forces the scheduler to re-evaluate the order. + /// Call this method if something changes + /// + public void Reschedule() + { + m_event.Set(); + } + + /// + /// A snapshot copy of the current schedule list + /// + public List> Schedule + { + get + { + lock (m_lock) + return m_schedule.ToList(); + } + } + + /// + /// A snapshot copy of the current worker queue, that is items that are scheduled, but waiting for execution + /// + public List WorkerQueue + { + get + { + return (from t in m_worker.CurrentTasks where t != null select t).ToList(); + } + } + + /// + /// Terminates the thread. Any items still in queue will be removed + /// + /// True if the call should block until the thread has exited, false otherwise + public void Terminate(bool wait) + { + m_terminate = true; + m_event.Set(); + + if (wait) + m_thread.Join(); + } + + /// + /// Returns the next valid date, given the start and the interval + /// + /// The base time + /// The first allowed date + /// The repetition interval + /// The days the backup is allowed to run + /// The next valid date, or throws an exception if no such date can be found + public static DateTime GetNextValidTime(DateTime basetime, DateTime firstdate, string repetition, DayOfWeek[] allowedDays) + { + var res = basetime; + + var i = 50000; + while (res < firstdate && i-- > 0) + res = Timeparser.ParseTimeInterval(repetition, res); + + // If we arrived somewhere after the first allowed date + if (res >= firstdate) + { + var ts = Timeparser.ParseTimeSpan(repetition); + + if (ts.TotalDays >= 1) + { + // We jump in days, so we pick the first valid day after firstdate + + for (var n = 0; n < 8; n++) + if (IsDateAllowed(res, allowedDays)) + break; + else + res = res.AddDays(1); + } + else + { + // We jump less than a day, so we keep adding the repetition until + // we hit a valid day + + i = 50000; + while (!IsDateAllowed(res, allowedDays) && i-- > 0) + res = Timeparser.ParseTimeInterval(repetition, res); + } + } + + if (!IsDateAllowed(res, allowedDays) || res < firstdate) + { + StringBuilder sb = new StringBuilder(); + if (allowedDays != null) + foreach (DayOfWeek w in allowedDays) + { + if (sb.Length != 0) + sb.Append(", "); + sb.Append(w.ToString()); + } + + throw new Exception(Strings.Scheduler.InvalidTimeSetupError(basetime, repetition, sb.ToString())); + } + + return res; + } + + private void OnCompleted(WorkerThread worker, Runner.IRunnerData task) + { + Tuple t = null; + lock(m_lock) + { + if (task != null && m_updateTasks.TryGetValue(task, out t)) + m_updateTasks.Remove(task); + } + + if (t != null) + { + t.Item1.Time = t.Item2; + t.Item1.LastRun = t.Item3; + FIXMEGlobal.DataConnection.AddOrUpdateSchedule(t.Item1); + } + + } + + private void OnStartingWork(WorkerThread worker, Runner.IRunnerData task) + { + if (task is null) + { + return; + } + + lock(m_lock) + { + if (m_updateTasks.TryGetValue(task, out Tuple scheduleInfo)) + { + // Item2 is the scheduled start time (Time in the Schedule table). + // Item3 is the actual start time (LastRun in the Schedule table). + m_updateTasks[task] = Tuple.Create(scheduleInfo.Item1, scheduleInfo.Item2, DateTime.UtcNow); + } + } + } + + /// + /// The actual scheduling procedure + /// + private void Runner() + { + var scheduled = new Dictionary>(); + while (!m_terminate) + { + //TODO: As this is executed repeatedly we should cache it + // to avoid frequent db lookups + + //Determine schedule list + var lst = FIXMEGlobal.DataConnection.Schedules; + foreach(var sc in lst) + { + if (!string.IsNullOrEmpty(sc.Repeat)) + { + KeyValuePair startkey; + + DateTime last = new DateTime(0, DateTimeKind.Utc); + DateTime start; + var scticks = sc.Time.Ticks; + + if (!scheduled.TryGetValue(sc.ID, out startkey) || startkey.Key != scticks) + { + start = new DateTime(scticks, DateTimeKind.Utc); + last = sc.LastRun; + } + else + { + start = startkey.Value; + } + + try + { + // Recover from timedrift issues by overriding the dates if the last run date is in the future. + if (last > DateTime.UtcNow) + { + start = DateTime.UtcNow; + last = DateTime.UtcNow; + } + start = GetNextValidTime(start, last, sc.Repeat, sc.AllowedDays); + } + catch (Exception ex) + { + FIXMEGlobal.DataConnection.LogError(sc.ID.ToString(), "Scheduler failed to find next date", ex); + } + + //If time is exceeded, run it now + if (start <= DateTime.UtcNow) + { + var jobsToRun = new List(); + //TODO: Cache this to avoid frequent lookups + foreach(var id in FIXMEGlobal.DataConnection.GetBackupIDsForTags(sc.Tags).Distinct().Select(x => x.ToString())) + { + //See if it is already queued + var tmplst = from n in m_worker.CurrentTasks + where n.Operation == Duplicati.Server.Serialization.DuplicatiOperation.Backup + select n.Backup; + var tastTemp = m_worker.CurrentTask; + if (tastTemp != null && tastTemp.Operation == Duplicati.Server.Serialization.DuplicatiOperation.Backup) + tmplst = tmplst.Union(new [] { tastTemp.Backup }); + + //If it is not already in queue, put it there + if (!tmplst.Any(x => x.ID == id)) + { + var entry = FIXMEGlobal.DataConnection.GetBackup(id); + if (entry != null) + { + Dictionary options = Duplicati.Server.Runner.GetCommonOptions(); + Duplicati.Server.Runner.ApplyOptions(entry, options); + if ((new Duplicati.Library.Main.Options(options)).DisableOnBattery && (Duplicati.Library.Utility.Power.PowerSupply.GetSource() == Duplicati.Library.Utility.Power.PowerSupply.Source.Battery)) + { + Duplicati.Library.Logging.Log.WriteInformationMessage(LOGTAG, "BackupDisabledOnBattery", "Scheduled backup disabled while on battery power."); + } + else + { + jobsToRun.Add(Server.Runner.CreateTask(Duplicati.Server.Serialization.DuplicatiOperation.Backup, entry)); + } + } + } + } + + // Calculate next time, by finding the first entry later than now + try + { + start = GetNextValidTime(start, new DateTime(Math.Max(DateTime.UtcNow.AddSeconds(1).Ticks, start.AddSeconds(1).Ticks), DateTimeKind.Utc), sc.Repeat, sc.AllowedDays); + } + catch(Exception ex) + { + FIXMEGlobal.DataConnection.LogError(sc.ID.ToString(), "Scheduler failed to find next date", ex); + continue; + } + + Server.Runner.IRunnerData lastJob = jobsToRun.LastOrDefault(); + if (lastJob != null) + { + lock (m_lock) + { + // The actual last run time will be updated when the StartingWork event is raised. + m_updateTasks[lastJob] = new Tuple(sc, start, DateTime.UtcNow); + } + } + + foreach (var job in jobsToRun) + m_worker.AddTask(job); + + if (start < DateTime.UtcNow) + { + //TODO: Report this somehow + continue; + } + } + + scheduled[sc.ID] = new KeyValuePair(scticks, start); + } + } + + var existing = lst.ToDictionary(x => x.ID); + //Sort them, lock as we assign the m_schedule variable + lock(m_lock) + m_schedule = (from n in scheduled + where existing.ContainsKey(n.Key) + orderby n.Value.Value + select new KeyValuePair(n.Value.Value, existing[n.Key])).ToArray(); + + // Remove unused entries + foreach(var c in (from n in scheduled where !existing.ContainsKey(n.Key) select n.Key).ToArray()) + scheduled.Remove(c); + + //Raise event if needed + if (NewSchedule != null) + NewSchedule(this, null); + + int waittime = 0; + + //Figure out a sensible amount of time to sleep the thread + if (scheduled.Count > 0) + { + //When is the next run scheduled? + TimeSpan nextrun = scheduled.Values.Min((x) => x.Value) - DateTime.UtcNow; + if (nextrun.TotalMilliseconds < 0) + continue; + + //Don't sleep for more than 5 minutes + waittime = (int)Math.Min(nextrun.TotalMilliseconds, 60 * 1000 * 5); + } + else + { + //No tasks, check back later + waittime = 60 * 1000; + } + + //Waiting on the event, enables a wakeup call from termination + // never use waittime = 0 + m_event.WaitOne(Math.Max(100, waittime), false); + } + } + + /// + /// Returns true if the time is at an allowed weekday, false otherwise + /// + /// The time to evaluate + /// The allowed days + /// True if the backup is allowed to run, false otherwise + private static bool IsDateAllowed(DateTime time, DayOfWeek[] allowedDays) + { + var localTime = time.ToLocalTime(); + if (allowedDays == null || allowedDays.Length == 0) + return true; + else + return Array.IndexOf(allowedDays, localTime.DayOfWeek) >= 0; + } + + } +} diff --git a/Duplicati/Server/Serializable/ImportExportStructure.cs b/Duplicati.Library.RestAPI/Serializable/ImportExportStructure.cs similarity index 97% rename from Duplicati/Server/Serializable/ImportExportStructure.cs rename to Duplicati.Library.RestAPI/Serializable/ImportExportStructure.cs index cd1ba632e..d737a721e 100644 --- a/Duplicati/Server/Serializable/ImportExportStructure.cs +++ b/Duplicati.Library.RestAPI/Serializable/ImportExportStructure.cs @@ -1,30 +1,30 @@ -// Copyright (C) 2015, The Duplicati Team - -// http://www.duplicati.com, info@duplicati.com -// -// This library is free software; you can redistribute it and/or modify -// it under the terms of the GNU Lesser General Public License as -// published by the Free Software Foundation; either version 2.1 of the -// License, or (at your option) any later version. -// -// This library is distributed in the hope that it will be useful, but -// WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU -// Lesser General Public License for more details. -// -// You should have received a copy of the GNU Lesser General Public -// License along with this library; if not, write to the Free Software -// Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA -using System.Collections.Generic; - -namespace Duplicati.Server.Serializable -{ - public class ImportExportStructure - { - public string CreatedByVersion { get; set; } - public Duplicati.Server.Database.Schedule Schedule { get; set; } - public Duplicati.Server.Database.Backup Backup { get; set; } - public Dictionary DisplayNames { get; set; } - } -} - +// Copyright (C) 2015, The Duplicati Team + +// http://www.duplicati.com, info@duplicati.com +// +// This library is free software; you can redistribute it and/or modify +// it under the terms of the GNU Lesser General Public License as +// published by the Free Software Foundation; either version 2.1 of the +// License, or (at your option) any later version. +// +// This library is distributed in the hope that it will be useful, but +// WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +// Lesser General Public License for more details. +// +// You should have received a copy of the GNU Lesser General Public +// License along with this library; if not, write to the Free Software +// Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA +using System.Collections.Generic; + +namespace Duplicati.Server.Serializable +{ + public class ImportExportStructure + { + public string CreatedByVersion { get; set; } + public Duplicati.Server.Database.Schedule Schedule { get; set; } + public Duplicati.Server.Database.Backup Backup { get; set; } + public Dictionary DisplayNames { get; set; } + } +} + diff --git a/Duplicati/Server/Serializable/ServerSettings.cs b/Duplicati.Library.RestAPI/Serializable/ServerSettings.cs similarity index 95% rename from Duplicati/Server/Serializable/ServerSettings.cs rename to Duplicati.Library.RestAPI/Serializable/ServerSettings.cs index 7571ce820..81dac0261 100644 --- a/Duplicati/Server/Serializable/ServerSettings.cs +++ b/Duplicati.Library.RestAPI/Serializable/ServerSettings.cs @@ -1,238 +1,239 @@ -// Copyright (C) 2015, The Duplicati Team - -// http://www.duplicati.com, info@duplicati.com -// -// This library is free software; you can redistribute it and/or modify -// it under the terms of the GNU Lesser General Public License as -// published by the Free Software Foundation; either version 2.1 of the -// License, or (at your option) any later version. -// -// This library is distributed in the hope that it will be useful, but -// WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU -// Lesser General Public License for more details. -// -// You should have received a copy of the GNU Lesser General Public -// License along with this library; if not, write to the Free Software -// Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA -using System; -using System.Linq; -using Duplicati.Server.Serialization.Interface; - -namespace Duplicati.Server.Serializable -{ - /// - /// The server config - /// - public static class ServerSettings - { - /// - /// Shared implementation for reporting dynamic modules - /// - private class DynamicModule : IDynamicModule - { - /// - /// Constructor for backend interface - /// - public DynamicModule(Duplicati.Library.Interface.IBackend backend) - { - this.Key = backend.ProtocolKey; - this.Description = backend.Description; - this.DisplayName = backend.DisplayName; - if (backend.SupportedCommands != null) - this.Options = backend.SupportedCommands.ToArray(); - } - - /// - /// Constructor for compression module interface - /// - public DynamicModule(Duplicati.Library.Interface.ICompression module) - { - this.Key = module.FilenameExtension; - this.Description = module.Description; - this.DisplayName = module.DisplayName; - if (module.SupportedCommands != null) - this.Options = module.SupportedCommands.ToArray(); - } - - /// - /// Constructor for encryption module interface - /// - public DynamicModule(Duplicati.Library.Interface.IEncryption module) - { - this.Key = module.FilenameExtension; - this.Description = module.Description; - this.DisplayName = module.DisplayName; - if (module.SupportedCommands != null) - this.Options = module.SupportedCommands.ToArray(); - } - - /// - /// Constructor for generic module interface - /// - public DynamicModule(Duplicati.Library.Interface.IGenericModule module) - { - this.Key = module.Key; - this.Description = module.Description; - this.DisplayName = module.DisplayName; - if (module.SupportedCommands != null) - this.Options = module.SupportedCommands.ToArray(); - } - - /// - /// Constructor for webmodule interface - /// - public DynamicModule(Duplicati.Library.Interface.IWebModule module) - { - this.Key = module.Key; - this.Description = module.Description; - this.DisplayName = module.DisplayName; - if (module.SupportedCommands != null) - this.Options = module.SupportedCommands.ToArray(); - } - /// - /// The module key - /// - public string Key { get; private set; } - /// - /// The localized module description - /// - public string Description { get; private set; } - /// - /// Gets the localized display name - /// - /// The display name. - public string DisplayName { get; private set; } - /// - /// The options supported by the module - /// - public Duplicati.Library.Interface.ICommandLineArgument[] Options { get; private set; } - } - - /// - /// Gets all supported options - /// - public static Duplicati.Library.Interface.ICommandLineArgument[] Options - { - get - { - return new Duplicati.Library.Main.Options(new System.Collections.Generic.Dictionary()).SupportedCommands.ToArray(); - } - } - - /// - /// The backend modules known by the server - /// - public static IDynamicModule[] BackendModules - { - get - { - return - (from n in Library.DynamicLoader.BackendLoader.Backends - select new DynamicModule(n)) - .ToArray(); - } - } - /// - /// The encryption modules known by the server - /// - public static IDynamicModule[] EncryptionModules - { - get - { - return - (from n in Library.DynamicLoader.EncryptionLoader.Modules - select new DynamicModule(n)) - .ToArray(); - } - } - - /// - /// The compression modules known by the server - /// - public static IDynamicModule[] CompressionModules - { - get - { - return - (from n in Library.DynamicLoader.CompressionLoader.Modules - select new DynamicModule(n)) - .ToArray(); - } - } - - /// - /// The generic modules known by the server - /// - public static IDynamicModule[] GenericModules - { - get - { - return - (from n in Library.DynamicLoader.GenericLoader.Modules - select new DynamicModule(n)) - .ToArray(); - } - } - - /// - /// The web modules known by the server - /// - public static IDynamicModule[] WebModules - { - get - { - return - (from n in Library.DynamicLoader.WebLoader.Modules - select new DynamicModule(n)) - .ToArray(); - } - } - - /// - /// The web modules known by the server - /// - public static IDynamicModule[] ConnectionModules - { - get - { - return - (from n in Library.DynamicLoader.GenericLoader.Modules - where n is Library.Interface.IConnectionModule - select new DynamicModule(n)) - .ToArray(); - } - } - - /// - /// The server modules known by the server - /// - public static object[] ServerModules - { - get - { - return - (from n in Library.DynamicLoader.GenericLoader.Modules - where n is Library.Interface.IGenericServerModule - select n) - .ToArray(); - } - } - - /// - /// The filters that are applied to all backups - /// - public static IFilter[] Filters - { - get { return Program.DataConnection.Filters; } - } - - /// - /// The settings applied to all backups by default - /// - public static ISetting[] Settings - { - get { return Program.DataConnection.Settings; } - } - } -} +// Copyright (C) 2015, The Duplicati Team + +// http://www.duplicati.com, info@duplicati.com +// +// This library is free software; you can redistribute it and/or modify +// it under the terms of the GNU Lesser General Public License as +// published by the Free Software Foundation; either version 2.1 of the +// License, or (at your option) any later version. +// +// This library is distributed in the hope that it will be useful, but +// WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +// Lesser General Public License for more details. +// +// You should have received a copy of the GNU Lesser General Public +// License along with this library; if not, write to the Free Software +// Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA +using System; +using System.Linq; +using Duplicati.Library.RestAPI; +using Duplicati.Server.Serialization.Interface; + +namespace Duplicati.Server.Serializable +{ + /// + /// The server config + /// + public static class ServerSettings + { + /// + /// Shared implementation for reporting dynamic modules + /// + private class DynamicModule : IDynamicModule + { + /// + /// Constructor for backend interface + /// + public DynamicModule(Duplicati.Library.Interface.IBackend backend) + { + this.Key = backend.ProtocolKey; + this.Description = backend.Description; + this.DisplayName = backend.DisplayName; + if (backend.SupportedCommands != null) + this.Options = backend.SupportedCommands.ToArray(); + } + + /// + /// Constructor for compression module interface + /// + public DynamicModule(Duplicati.Library.Interface.ICompression module) + { + this.Key = module.FilenameExtension; + this.Description = module.Description; + this.DisplayName = module.DisplayName; + if (module.SupportedCommands != null) + this.Options = module.SupportedCommands.ToArray(); + } + + /// + /// Constructor for encryption module interface + /// + public DynamicModule(Duplicati.Library.Interface.IEncryption module) + { + this.Key = module.FilenameExtension; + this.Description = module.Description; + this.DisplayName = module.DisplayName; + if (module.SupportedCommands != null) + this.Options = module.SupportedCommands.ToArray(); + } + + /// + /// Constructor for generic module interface + /// + public DynamicModule(Duplicati.Library.Interface.IGenericModule module) + { + this.Key = module.Key; + this.Description = module.Description; + this.DisplayName = module.DisplayName; + if (module.SupportedCommands != null) + this.Options = module.SupportedCommands.ToArray(); + } + + /// + /// Constructor for webmodule interface + /// + public DynamicModule(Duplicati.Library.Interface.IWebModule module) + { + this.Key = module.Key; + this.Description = module.Description; + this.DisplayName = module.DisplayName; + if (module.SupportedCommands != null) + this.Options = module.SupportedCommands.ToArray(); + } + /// + /// The module key + /// + public string Key { get; private set; } + /// + /// The localized module description + /// + public string Description { get; private set; } + /// + /// Gets the localized display name + /// + /// The display name. + public string DisplayName { get; private set; } + /// + /// The options supported by the module + /// + public Duplicati.Library.Interface.ICommandLineArgument[] Options { get; private set; } + } + + /// + /// Gets all supported options + /// + public static Duplicati.Library.Interface.ICommandLineArgument[] Options + { + get + { + return new Duplicati.Library.Main.Options(new System.Collections.Generic.Dictionary()).SupportedCommands.ToArray(); + } + } + + /// + /// The backend modules known by the server + /// + public static IDynamicModule[] BackendModules + { + get + { + return + (from n in Library.DynamicLoader.BackendLoader.Backends + select new DynamicModule(n)) + .ToArray(); + } + } + /// + /// The encryption modules known by the server + /// + public static IDynamicModule[] EncryptionModules + { + get + { + return + (from n in Library.DynamicLoader.EncryptionLoader.Modules + select new DynamicModule(n)) + .ToArray(); + } + } + + /// + /// The compression modules known by the server + /// + public static IDynamicModule[] CompressionModules + { + get + { + return + (from n in Library.DynamicLoader.CompressionLoader.Modules + select new DynamicModule(n)) + .ToArray(); + } + } + + /// + /// The generic modules known by the server + /// + public static IDynamicModule[] GenericModules + { + get + { + return + (from n in Library.DynamicLoader.GenericLoader.Modules + select new DynamicModule(n)) + .ToArray(); + } + } + + /// + /// The web modules known by the server + /// + public static IDynamicModule[] WebModules + { + get + { + return + (from n in Library.DynamicLoader.WebLoader.Modules + select new DynamicModule(n)) + .ToArray(); + } + } + + /// + /// The web modules known by the server + /// + public static IDynamicModule[] ConnectionModules + { + get + { + return + (from n in Library.DynamicLoader.GenericLoader.Modules + where n is Library.Interface.IConnectionModule + select new DynamicModule(n)) + .ToArray(); + } + } + + /// + /// The server modules known by the server + /// + public static object[] ServerModules + { + get + { + return + (from n in Library.DynamicLoader.GenericLoader.Modules + where n is Library.Interface.IGenericServerModule + select n) + .ToArray(); + } + } + + /// + /// The filters that are applied to all backups + /// + public static IFilter[] Filters + { + get { return FIXMEGlobal.DataConnection.Filters; } + } + + /// + /// The settings applied to all backups by default + /// + public static ISetting[] Settings + { + get { return FIXMEGlobal.DataConnection.Settings; } + } + } +} diff --git a/Duplicati/Server/Serializable/ServerStatus.cs b/Duplicati.Library.RestAPI/Serializable/ServerStatus.cs similarity index 76% rename from Duplicati/Server/Serializable/ServerStatus.cs rename to Duplicati.Library.RestAPI/Serializable/ServerStatus.cs index 378225270..ff397d40f 100644 --- a/Duplicati/Server/Serializable/ServerStatus.cs +++ b/Duplicati.Library.RestAPI/Serializable/ServerStatus.cs @@ -1,146 +1,147 @@ -#region "Disclaimer / License" -// Copyright (C) 2015, The Duplicati Team - -// http://www.duplicati.com, info@duplicati.com -// -// This library is free software; you can redistribute it and/or modify -// it under the terms of the GNU Lesser General Public License as -// published by the Free Software Foundation; either version 2.1 of the -// License, or (at your option) any later version. -// -// This library is distributed in the hope that it will be useful, but -// WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU -// Lesser General Public License for more details. -// -// You should have received a copy of the GNU Lesser General Public -// License along with this library; if not, write to the Free Software -// Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA -#endregion -using System; -using System.Linq; -using System.Collections.Generic; -using Duplicati.Server.Serialization; - -namespace Duplicati.Server.Serializable -{ - /// - /// This class collects all reportable status properties into a single class that can be exported as JSON - /// - public class ServerStatus : Duplicati.Server.Serialization.Interface.IServerStatus - { - public LiveControlState ProgramState - { - get { return EnumConverter.Convert(Program.LiveControl.State); } - } - - public string UpdatedVersion - { - get - { - var u = Program.DataConnection.ApplicationSettings.UpdatedVersion; - if (u == null) - return null; - - Version v; - if (!Version.TryParse(u.Version, out v)) - return null; - - if (v <= System.Reflection.Assembly.GetExecutingAssembly().GetName().Version) - return null; - - return u.Displayname; - } - } - - public UpdatePollerStates UpdaterState { get { return Program.UpdatePoller.ThreadState; } } - - public bool UpdateReady { get { return Duplicati.Library.AutoUpdater.UpdaterManager.HasUpdateInstalled; } } - - public double UpdateDownloadProgress { get { return Program.UpdatePoller.DownloadProgess; } } - - - public Tuple ActiveTask - { - get - { - var t = Program.WorkThread.CurrentTask; - if (t == null) - return null; - else - return new Tuple(t.TaskID, t.Backup == null ? null : t.Backup.ID); - } - } - - public IList> SchedulerQueueIds - { - get { return (from n in Program.Scheduler.WorkerQueue where n.Backup != null select new Tuple(n.TaskID, n.Backup.ID)).ToList(); } - } - - public IList> ProposedSchedule - { - get - { - return ( - from n in Program.Scheduler.Schedule - let backupid = (from t in n.Value.Tags - where t != null && t.StartsWith("ID=", StringComparison.Ordinal) - select t.Substring("ID=".Length)).FirstOrDefault() - where !string.IsNullOrWhiteSpace(backupid) - select new Tuple(backupid, n.Key) - ).ToList(); - } - } - - public bool HasWarning { get { return Program.DataConnection.ApplicationSettings.UnackedWarning; } } - public bool HasError { get { return Program.DataConnection.ApplicationSettings.UnackedError; } } - - public SuggestedStatusIcon SuggestedStatusIcon - { - get - { - if (this.ActiveTask == null) - { - if (this.ProgramState == LiveControlState.Paused) - return SuggestedStatusIcon.Paused; - - if (this.HasError) - return SuggestedStatusIcon.ReadyError; - if (this.HasWarning) - return SuggestedStatusIcon.ReadyWarning; - - return SuggestedStatusIcon.Ready; - } - else - { - if (this.ProgramState == LiveControlState.Running) - return SuggestedStatusIcon.Active; - else - return SuggestedStatusIcon.ActivePaused; - } - } - } - - public DateTime EstimatedPauseEnd - { - get - { - return Program.LiveControl.EstimatedPauseEnd; - } - } - - private long m_lastEventID = Program.StatusEventNotifyer.EventNo; - - public long LastEventID - { - get { return m_lastEventID; } - set { m_lastEventID = value; } - } - - public long LastDataUpdateID { get { return Program.LastDataUpdateID; } } - - public long LastNotificationUpdateID { get { return Program.LastNotificationUpdateID; } } - - } -} - +#region "Disclaimer / License" +// Copyright (C) 2015, The Duplicati Team + +// http://www.duplicati.com, info@duplicati.com +// +// This library is free software; you can redistribute it and/or modify +// it under the terms of the GNU Lesser General Public License as +// published by the Free Software Foundation; either version 2.1 of the +// License, or (at your option) any later version. +// +// This library is distributed in the hope that it will be useful, but +// WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +// Lesser General Public License for more details. +// +// You should have received a copy of the GNU Lesser General Public +// License along with this library; if not, write to the Free Software +// Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA +#endregion +using System; +using System.Linq; +using System.Collections.Generic; +using Duplicati.Server.Serialization; +using Duplicati.Library.RestAPI; + +namespace Duplicati.Server.Serializable +{ + /// + /// This class collects all reportable status properties into a single class that can be exported as JSON + /// + public class ServerStatus : Duplicati.Server.Serialization.Interface.IServerStatus + { + public LiveControlState ProgramState + { + get { return EnumConverter.Convert(FIXMEGlobal.LiveControl.State); } + } + + public string UpdatedVersion + { + get + { + var u = FIXMEGlobal.DataConnection.ApplicationSettings.UpdatedVersion; + if (u == null) + return null; + + Version v; + if (!Version.TryParse(u.Version, out v)) + return null; + + if (v <= System.Reflection.Assembly.GetExecutingAssembly().GetName().Version) + return null; + + return u.Displayname; + } + } + + public UpdatePollerStates UpdaterState { get { return FIXMEGlobal.UpdatePoller.ThreadState; } } + + public bool UpdateReady { get { return Duplicati.Library.AutoUpdater.UpdaterManager.HasUpdateInstalled; } } + + public double UpdateDownloadProgress { get { return FIXMEGlobal.UpdatePoller.DownloadProgess; } } + + + public Tuple ActiveTask + { + get + { + var t = FIXMEGlobal.WorkThread.CurrentTask; + if (t == null) + return null; + else + return new Tuple(t.TaskID, t.Backup == null ? null : t.Backup.ID); + } + } + + public IList> SchedulerQueueIds + { + get { return (from n in FIXMEGlobal.Scheduler.WorkerQueue where n.Backup != null select new Tuple(n.TaskID, n.Backup.ID)).ToList(); } + } + + public IList> ProposedSchedule + { + get + { + return ( + from n in FIXMEGlobal.Scheduler.Schedule + let backupid = (from t in n.Value.Tags + where t != null && t.StartsWith("ID=", StringComparison.Ordinal) + select t.Substring("ID=".Length)).FirstOrDefault() + where !string.IsNullOrWhiteSpace(backupid) + select new Tuple(backupid, n.Key) + ).ToList(); + } + } + + public bool HasWarning { get { return FIXMEGlobal.DataConnection.ApplicationSettings.UnackedWarning; } } + public bool HasError { get { return FIXMEGlobal.DataConnection.ApplicationSettings.UnackedError; } } + + public SuggestedStatusIcon SuggestedStatusIcon + { + get + { + if (this.ActiveTask == null) + { + if (this.ProgramState == LiveControlState.Paused) + return SuggestedStatusIcon.Paused; + + if (this.HasError) + return SuggestedStatusIcon.ReadyError; + if (this.HasWarning) + return SuggestedStatusIcon.ReadyWarning; + + return SuggestedStatusIcon.Ready; + } + else + { + if (this.ProgramState == LiveControlState.Running) + return SuggestedStatusIcon.Active; + else + return SuggestedStatusIcon.ActivePaused; + } + } + } + + public DateTime EstimatedPauseEnd + { + get + { + return FIXMEGlobal.LiveControl.EstimatedPauseEnd; + } + } + + private long m_lastEventID = FIXMEGlobal.StatusEventNotifyer.EventNo; + + public long LastEventID + { + get { return m_lastEventID; } + set { m_lastEventID = value; } + } + + public long LastDataUpdateID { get { return FIXMEGlobal.PeekLastDataUpdateID(); } } + + public long LastNotificationUpdateID { get { return FIXMEGlobal.PeekLastNotificationUpdateID(); } } + + } +} + diff --git a/Duplicati/Server/Serializable/TreeNode.cs b/Duplicati.Library.RestAPI/Serializable/TreeNode.cs similarity index 96% rename from Duplicati/Server/Serializable/TreeNode.cs rename to Duplicati.Library.RestAPI/Serializable/TreeNode.cs index 39c49d05e..02a107971 100644 --- a/Duplicati/Server/Serializable/TreeNode.cs +++ b/Duplicati.Library.RestAPI/Serializable/TreeNode.cs @@ -1,60 +1,60 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; - -namespace Duplicati.Server.Serializable -{ - /// - /// Implementation of a ExtJS treenode-like class for easy JSON export - /// - public class TreeNode - { - /// - /// The text displayed for the node - /// - public string text { get; set; } - /// - /// The node id - /// - public string id { get; set; } - /// - /// The class applied to the node - /// - public string cls { get; set; } - /// - /// The class applied to the icon - /// - public string iconCls { get; set; } - /// - /// True if the element should be checked - /// - public bool check { get; set; } - /// - /// True if the element is a leaf node - /// - public bool leaf { get; set; } - /// - /// Gets or sets the current path, if the item is a symbolic path - /// - public string resolvedpath { get; set; } - /// - /// True if the element is hidden - /// - public bool hidden { get; set; } - /// - /// True if the element is a symlink - /// - public bool symlink { get; set; } - - /// - /// Constructs a new TreeNode - /// - public TreeNode() - { - this.cls = "folder"; - this.iconCls = "x-tree-icon-parent"; - this.check = false; - } - } -} +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; + +namespace Duplicati.Server.Serializable +{ + /// + /// Implementation of a ExtJS treenode-like class for easy JSON export + /// + public class TreeNode + { + /// + /// The text displayed for the node + /// + public string text { get; set; } + /// + /// The node id + /// + public string id { get; set; } + /// + /// The class applied to the node + /// + public string cls { get; set; } + /// + /// The class applied to the icon + /// + public string iconCls { get; set; } + /// + /// True if the element should be checked + /// + public bool check { get; set; } + /// + /// True if the element is a leaf node + /// + public bool leaf { get; set; } + /// + /// Gets or sets the current path, if the item is a symbolic path + /// + public string resolvedpath { get; set; } + /// + /// True if the element is hidden + /// + public bool hidden { get; set; } + /// + /// True if the element is a symlink + /// + public bool symlink { get; set; } + + /// + /// Constructs a new TreeNode + /// + public TreeNode() + { + this.cls = "folder"; + this.iconCls = "x-tree-icon-parent"; + this.check = false; + } + } +} diff --git a/Duplicati/Server/SpecialFolders.cs b/Duplicati.Library.RestAPI/SpecialFolders.cs similarity index 97% rename from Duplicati/Server/SpecialFolders.cs rename to Duplicati.Library.RestAPI/SpecialFolders.cs index e95940d26..882f88ebc 100644 --- a/Duplicati/Server/SpecialFolders.cs +++ b/Duplicati.Library.RestAPI/SpecialFolders.cs @@ -1,191 +1,191 @@ -// Copyright (C) 2015, The Duplicati Team - -// http://www.duplicati.com, info@duplicati.com -// -// This library is free software; you can redistribute it and/or modify -// it under the terms of the GNU Lesser General Public License as -// published by the Free Software Foundation; either version 2.1 of the -// License, or (at your option) any later version. -// -// This library is distributed in the hope that it will be useful, but -// WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU -// Lesser General Public License for more details. -// -// You should have received a copy of the GNU Lesser General Public -// License along with this library; if not, write to the Free Software -// Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA -using System; -using System.Linq; -using System.Collections.Generic; +// Copyright (C) 2015, The Duplicati Team + +// http://www.duplicati.com, info@duplicati.com +// +// This library is free software; you can redistribute it and/or modify +// it under the terms of the GNU Lesser General Public License as +// published by the Free Software Foundation; either version 2.1 of the +// License, or (at your option) any later version. +// +// This library is distributed in the hope that it will be useful, but +// WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +// Lesser General Public License for more details. +// +// You should have received a copy of the GNU Lesser General Public +// License along with this library; if not, write to the Free Software +// Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA +using System; +using System.Linq; +using System.Collections.Generic; using Duplicati.Library.Common.IO; using Duplicati.Library.Common; -namespace Duplicati.Server -{ - public static class SpecialFolders - { - public static readonly Serializable.TreeNode[] Nodes; - private static readonly Dictionary PathMap = new Dictionary(StringComparer.OrdinalIgnoreCase); - private static readonly Dictionary DisplayMap = new Dictionary(StringComparer.OrdinalIgnoreCase); - - public static string ExpandEnvironmentVariables(string path) - { - foreach(var n in Nodes) - if (path.StartsWith(n.id, StringComparison.Ordinal)) - path = path.Replace(n.id, n.resolvedpath); - return Environment.ExpandEnvironmentVariables(path); - } - - public static string ExpandEnvironmentVariablesRegexp(string path) - { - // The double expand is to use both the special folder names, - // which are not in the environment, as well as allow expansion - // of values found in the environment - - return - Library.Utility.Utility.ExpandEnvironmentVariablesRegexp(path, name => - { - var res = string.Empty; - if (name != null && !PathMap.TryGetValue(name, out res)) - res = Environment.GetEnvironmentVariable(name); - - return res; - }); - } - - public static string TranslateToPath(string str) - { - string res; - if (PathMap.TryGetValue(str, out res)) - return res; - - return null; - } - - public static string TranslateToDisplayString(string str) - { - string res; - if (DisplayMap.TryGetValue(str, out res)) - return res; - - return null; - } - - private static void TryAdd(List lst, System.Environment.SpecialFolder folder, string id, string display) - { - try - { - TryAdd(lst, System.Environment.GetFolderPath(folder), id, display); - } - catch - { - } - } - - private static void TryAdd(List lst, string folder, string id, string display) - { - try - { - if (!string.IsNullOrWhiteSpace(folder) && System.IO.Path.IsPathRooted(folder) && System.IO.Directory.Exists(folder)) - { - if (!PathMap.ContainsKey(id)) - { - lst.Add(new Serializable.TreeNode() - { - id = id, - text = display, - leaf = false, - iconCls = "x-tree-icon-special", - resolvedpath = folder - }); - - PathMap[id] = folder; - DisplayMap[id] = display; - } - } - } - catch - { - } - } - - static SpecialFolders() - { - var lst = new List(); - - if (Platform.IsClientWindows) - { - TryAdd(lst, Environment.SpecialFolder.MyDocuments, "%MY_DOCUMENTS%", "My Documents"); - TryAdd(lst, Environment.SpecialFolder.MyMusic, "%MY_MUSIC%", "My Music"); - TryAdd(lst, Environment.SpecialFolder.MyPictures, "%MY_PICTURES%", "My Pictures"); - TryAdd(lst, Environment.SpecialFolder.MyVideos, "%MY_VIDEOS%", "My Videos"); - TryAdd(lst, Environment.SpecialFolder.DesktopDirectory, "%DESKTOP%", "Desktop"); - TryAdd(lst, Environment.SpecialFolder.ApplicationData, "%APPDATA%", "Application Data"); - TryAdd(lst, Environment.SpecialFolder.UserProfile, "%HOME%", "Home"); - - try - { - // In case the UserProfile member points to junk - TryAdd(lst, System.IO.Path.Combine(Environment.GetEnvironmentVariable("HOMEDRIVE"), Environment.GetEnvironmentVariable("HOMEPATH")), "%HOME%", "Home"); - } - catch - { - } - - } - else - { - TryAdd(lst, Environment.SpecialFolder.MyDocuments, "%MY_DOCUMENTS%", "My Documents"); - TryAdd(lst, Environment.SpecialFolder.MyMusic, "%MY_MUSIC%", "My Music"); - TryAdd(lst, Environment.SpecialFolder.MyPictures, "%MY_PICTURES%", "My Pictures"); - TryAdd(lst, Environment.SpecialFolder.DesktopDirectory, "%DESKTOP%", "Desktop"); - TryAdd(lst, Environment.GetEnvironmentVariable("HOME"), "%HOME%", "Home"); - TryAdd(lst, Environment.SpecialFolder.Personal, "%HOME%", "Home"); - } - - Nodes = lst.ToArray(); - } - - internal static Dictionary GetSourceNames(Serialization.Interface.IBackup backup) - { - if (backup.Sources == null || backup.Sources.Length == 0) - return new Dictionary(); - - var sources = backup.Sources.Distinct().Select(x => - { - var sp = SpecialFolders.TranslateToDisplayString(x); - if (sp != null) - return new KeyValuePair(x, sp); - - x = SpecialFolders.ExpandEnvironmentVariables(x); - try - { - var nx = x; - if (nx.EndsWith(Util.DirectorySeparatorString, StringComparison.Ordinal)) - nx = nx.Substring(0, nx.Length - 1); - var n = SystemIO.IO_OS.PathGetFileName(nx); - if (!string.IsNullOrWhiteSpace(n)) - return new KeyValuePair(x, n); - } - catch - { - } - - if (x.EndsWith(Util.DirectorySeparatorString, StringComparison.Ordinal) && x.Length > 1) - return new KeyValuePair(x, x.Substring(0, x.Length - 1).Substring(x.Substring(0, x.Length - 1).LastIndexOf("/", StringComparison.Ordinal) + 1)); - else - return new KeyValuePair(x, x); - - }); - - // Handle duplicates - var result = new Dictionary(); - foreach(var x in sources) - result[x.Key] = x.Value; - - return result; - } - } -} - +namespace Duplicati.Server +{ + public static class SpecialFolders + { + public static readonly Serializable.TreeNode[] Nodes; + private static readonly Dictionary PathMap = new Dictionary(StringComparer.OrdinalIgnoreCase); + private static readonly Dictionary DisplayMap = new Dictionary(StringComparer.OrdinalIgnoreCase); + + public static string ExpandEnvironmentVariables(string path) + { + foreach(var n in Nodes) + if (path.StartsWith(n.id, StringComparison.Ordinal)) + path = path.Replace(n.id, n.resolvedpath); + return Environment.ExpandEnvironmentVariables(path); + } + + public static string ExpandEnvironmentVariablesRegexp(string path) + { + // The double expand is to use both the special folder names, + // which are not in the environment, as well as allow expansion + // of values found in the environment + + return + Library.Utility.Utility.ExpandEnvironmentVariablesRegexp(path, name => + { + var res = string.Empty; + if (name != null && !PathMap.TryGetValue(name, out res)) + res = Environment.GetEnvironmentVariable(name); + + return res; + }); + } + + public static string TranslateToPath(string str) + { + string res; + if (PathMap.TryGetValue(str, out res)) + return res; + + return null; + } + + public static string TranslateToDisplayString(string str) + { + string res; + if (DisplayMap.TryGetValue(str, out res)) + return res; + + return null; + } + + private static void TryAdd(List lst, System.Environment.SpecialFolder folder, string id, string display) + { + try + { + TryAdd(lst, System.Environment.GetFolderPath(folder), id, display); + } + catch + { + } + } + + private static void TryAdd(List lst, string folder, string id, string display) + { + try + { + if (!string.IsNullOrWhiteSpace(folder) && System.IO.Path.IsPathRooted(folder) && System.IO.Directory.Exists(folder)) + { + if (!PathMap.ContainsKey(id)) + { + lst.Add(new Serializable.TreeNode() + { + id = id, + text = display, + leaf = false, + iconCls = "x-tree-icon-special", + resolvedpath = folder + }); + + PathMap[id] = folder; + DisplayMap[id] = display; + } + } + } + catch + { + } + } + + static SpecialFolders() + { + var lst = new List(); + + if (Platform.IsClientWindows) + { + TryAdd(lst, Environment.SpecialFolder.MyDocuments, "%MY_DOCUMENTS%", "My Documents"); + TryAdd(lst, Environment.SpecialFolder.MyMusic, "%MY_MUSIC%", "My Music"); + TryAdd(lst, Environment.SpecialFolder.MyPictures, "%MY_PICTURES%", "My Pictures"); + TryAdd(lst, Environment.SpecialFolder.MyVideos, "%MY_VIDEOS%", "My Videos"); + TryAdd(lst, Environment.SpecialFolder.DesktopDirectory, "%DESKTOP%", "Desktop"); + TryAdd(lst, Environment.SpecialFolder.ApplicationData, "%APPDATA%", "Application Data"); + TryAdd(lst, Environment.SpecialFolder.UserProfile, "%HOME%", "Home"); + + try + { + // In case the UserProfile member points to junk + TryAdd(lst, System.IO.Path.Combine(Environment.GetEnvironmentVariable("HOMEDRIVE"), Environment.GetEnvironmentVariable("HOMEPATH")), "%HOME%", "Home"); + } + catch + { + } + + } + else + { + TryAdd(lst, Environment.SpecialFolder.MyDocuments, "%MY_DOCUMENTS%", "My Documents"); + TryAdd(lst, Environment.SpecialFolder.MyMusic, "%MY_MUSIC%", "My Music"); + TryAdd(lst, Environment.SpecialFolder.MyPictures, "%MY_PICTURES%", "My Pictures"); + TryAdd(lst, Environment.SpecialFolder.DesktopDirectory, "%DESKTOP%", "Desktop"); + TryAdd(lst, Environment.GetEnvironmentVariable("HOME"), "%HOME%", "Home"); + TryAdd(lst, Environment.SpecialFolder.Personal, "%HOME%", "Home"); + } + + Nodes = lst.ToArray(); + } + + internal static Dictionary GetSourceNames(Serialization.Interface.IBackup backup) + { + if (backup.Sources == null || backup.Sources.Length == 0) + return new Dictionary(); + + var sources = backup.Sources.Distinct().Select(x => + { + var sp = SpecialFolders.TranslateToDisplayString(x); + if (sp != null) + return new KeyValuePair(x, sp); + + x = SpecialFolders.ExpandEnvironmentVariables(x); + try + { + var nx = x; + if (nx.EndsWith(Util.DirectorySeparatorString, StringComparison.Ordinal)) + nx = nx.Substring(0, nx.Length - 1); + var n = SystemIO.IO_OS.PathGetFileName(nx); + if (!string.IsNullOrWhiteSpace(n)) + return new KeyValuePair(x, n); + } + catch + { + } + + if (x.EndsWith(Util.DirectorySeparatorString, StringComparison.Ordinal) && x.Length > 1) + return new KeyValuePair(x, x.Substring(0, x.Length - 1).Substring(x.Substring(0, x.Length - 1).LastIndexOf("/", StringComparison.Ordinal) + 1)); + else + return new KeyValuePair(x, x); + + }); + + // Handle duplicates + var result = new Dictionary(); + foreach(var x in sources) + result[x.Key] = x.Value; + + return result; + } + } +} + diff --git a/Duplicati/Server/Strings.cs b/Duplicati.Library.RestAPI/Strings.cs similarity index 98% rename from Duplicati/Server/Strings.cs rename to Duplicati.Library.RestAPI/Strings.cs index 829d6a3c2..d925113eb 100644 --- a/Duplicati/Server/Strings.cs +++ b/Duplicati.Library.RestAPI/Strings.cs @@ -1,57 +1,57 @@ -using System.Collections.Generic; -using Duplicati.Library.Localization.Short; -using System.Linq; - -namespace Duplicati.Server.Strings { - internal static class Program { - public static string AnotherInstanceDetected { get { return LC.L(@"Another instance is running, and was notified"); } } - public static string DatabaseOpenError(string message) { return LC.L(@"Failed to create, open or upgrade the database. -Error message: {0}", message); } - public static string HelpCommandDescription { get { return LC.L(@"Displays this help"); } } - public static string HelpDisplayDialog { get { return LC.L(@"Supported commandline arguments: - -"); } } - public static string HelpDisplayFormat(string optionname, string optiontext) { return LC.L(@"--{0}: {1}", optionname, optiontext); } - public static string ParametersFileOptionLong2 { get { return LC.L(@"This option can be used to store some or all of the options given to the commandline client. The file must be a plain text file, UTF-8 encoding is preferred. Each line in the file should be of the format --option=value. The special options --{0} and --{1} can be used to override the localpath and the remote destination uri, respectively. The options in this file take precedence over the options provided on the commandline. You cannot specify filters in both the file and on the commandline. Instead, you can use the special --{2}, --{3}, or --{4} options to specify filters inside the parameter file. Each filter must be prefixed with either a + or a -, and multiple filters must be joined with {5} ", "source", "target", "replace-filter", "append-filter", "prepend-filter", System.IO.Path.PathSeparator); } } - public static string ParametersFileOptionShort { get { return LC.L(@"Path to a file with parameters"); } } - public static string FiltersCannotBeUsedWithFileError2 { get { return LC.L(@"Filters cannot be specified on the commandline if filters are also present in the parameter file. Use the special --{0}, --{1}, or --{2} options to specify filters inside the parameter file. Each filter must be prefixed with either a + or a -, and multiple filters must be joined with {3}", "replace-filter", "append-filter", "prepend-filter", System.IO.Path.PathSeparator); } } - public static string FailedToParseParametersFileError(string path, string message) { return LC.L(@"Unable to read the parameters file ""{0}"", reason: {1}", path, message); } - public static string SkippingSourceArgumentsOnNonBackupOperation { get { return @"The --source argument was specified in the parameter file, but the current operation is not a backup operation, so the argument is ignored"; } } - public static string LogfileCommandDescription { get { return LC.L(@"Outputs log information to the file given"); } } - public static string LoglevelCommandDescription { get { return LC.L(@"Determines the amount of information written in the log file"); } } - public static string PortablemodeCommandDescription { get { return LC.L(@"Activates portable mode where the database is placed below the program executable"); } } - public static string SeriousError(string message) { return LC.L(@"A serious error occurred in Duplicati: {0}", message); } - public static string StartupFailure(System.Exception error) { return LC.L(@"Unable to start up, perhaps another process is already running? -Error message: {0}", error); } - public static string UnencrypteddatabaseCommandDescription { get { return LC.L(@"Disables database encryption"); } } - public static string WrongSQLiteVersion(System.Version actualversion, string expectedversion) { return LC.L(@"Unsupported version of SQLite detected ({0}), must be {1} or higher", actualversion, expectedversion); } - public static string WebserverWebrootDescription { get { return LC.L(@"The path to the folder where the static files for the webserver is present. The folder must be located beneath the installation folder"); } } - public static string WebserverPortDescription { get { return LC.L(@"The port the webserver listens on. Multiple values may be supplied with a comma in between."); } } - public static string WebserverCertificateFileDescription { get { return LC.L(@"The certificate and key file in PKCS #12 format the webserver use for SSL. Only RSA/DSA keys are supported."); } } - public static string WebserverCertificatePasswordDescription { get { return LC.L(@"The password for decryption of certificate PKCS #12 file."); } } - public static string WebserverInterfaceDescription { get { return LC.L(@"The interface the webserver listens on. The special values ""*"" and ""any"" means any interface. The special value ""loopback"" means the loopback adapter."); } } - public static string WebserverPasswordDescription { get { return LC.L(@"The password required to access the webserver. This option is saved so you do not need to set it on each run. Setting an empty value disables the password."); } } - public static string WebserverAllowedhostnamesDescription { get { return LC.L(@"The hostnames that are accepted, separated with semicolons. If any of the hostnames are ""*"", all hostnames are allowed and the hostname checking is disabled."); } } - public static string PingpongkeepaliveShort { get { return LC.L(@"Enables the ping-pong responder"); } } - public static string PingpongkeepaliveLong { get { return LC.L(@"When running as a server, the service daemon must verify that the process is responding. If this option is enabled, the server reads stdin and writes a reply to each line read"); } } - public static string LogretentionShort { get { return LC.L(@"Clean up old log data"); } } - public static string LogretentionLong { get { return LC.L(@"Set the time after which log data will be purged from the database."); } } - public static string ServerdatafolderShort { get { return LC.L(@"Sets the folder where settings are stored"); } } - public static string ServerdatafolderLong(string envname) { return LC.L(@"Duplicati needs to store a small database with all settings. Use this option to choose where the settings are stored. This option can also be set with the environment variable {0}.", envname); } - public static string ServerencryptionkeyShort { get { return LC.L(@"Sets the database encryption key"); } } - public static string ServerencryptionkeyLong(string envname, string decryptionoption) { return LC.L(@"This option sets the encryption key used to scramble the local settings database. This option can also be set with the environment variable {0}. Use the option --{1} to disable the database scrambling.", envname, decryptionoption); } - public static string TempdirShort { get { return LC.L(@"Temporary storage folder"); } } - public static string TempdirLong { get { return LC.L(@"This option can be used to supply an alternative folder for temporary storage. By default the system default temporary folder is used. Note that also SQLite will put temporary files in this temporary folder."); } } -} - internal static class Scheduler { - public static string InvalidTimeSetupError(System.DateTime startdate, string interval, string alloweddays) { return LC.L(@"Unable to find a valid date, given the start date {0}, the repetition interval {1} and the allowed days {2}", startdate, interval, alloweddays); } - } - internal static class Server - { - public static string DefectSSLCertInDatabase { get { return @"Unable to create SSL certificate using data from database. Starting without SSL."; } } - public static string StartedServer(string ip, int port) { return LC.L(@"Server has started and is listening on {0}, port {1}", ip, port); } - public static string SSLCertificateFailure(string errormessage) { return LC.L(@"Unable to create SSL certificate using provided parameters. Exception detail: {0}", errormessage); } - public static string ServerStartFailure(IEnumerable portstried) { return LC.L(@"Unable to open a socket for listening, tried ports: {0}", string.Join(",", from n in (portstried ?? new int[0]) select n.ToString())); } - } - -} +using System.Collections.Generic; +using Duplicati.Library.Localization.Short; +using System.Linq; + +namespace Duplicati.Server.Strings { + public static class Program { + public static string AnotherInstanceDetected { get { return LC.L(@"Another instance is running, and was notified"); } } + public static string DatabaseOpenError(string message) { return LC.L(@"Failed to create, open or upgrade the database. +Error message: {0}", message); } + public static string HelpCommandDescription { get { return LC.L(@"Displays this help"); } } + public static string HelpDisplayDialog { get { return LC.L(@"Supported commandline arguments: + +"); } } + public static string HelpDisplayFormat(string optionname, string optiontext) { return LC.L(@"--{0}: {1}", optionname, optiontext); } + public static string ParametersFileOptionLong2 { get { return LC.L(@"This option can be used to store some or all of the options given to the commandline client. The file must be a plain text file, UTF-8 encoding is preferred. Each line in the file should be of the format --option=value. The special options --{0} and --{1} can be used to override the localpath and the remote destination uri, respectively. The options in this file take precedence over the options provided on the commandline. You cannot specify filters in both the file and on the commandline. Instead, you can use the special --{2}, --{3}, or --{4} options to specify filters inside the parameter file. Each filter must be prefixed with either a + or a -, and multiple filters must be joined with {5} ", "source", "target", "replace-filter", "append-filter", "prepend-filter", System.IO.Path.PathSeparator); } } + public static string ParametersFileOptionShort { get { return LC.L(@"Path to a file with parameters"); } } + public static string FiltersCannotBeUsedWithFileError2 { get { return LC.L(@"Filters cannot be specified on the commandline if filters are also present in the parameter file. Use the special --{0}, --{1}, or --{2} options to specify filters inside the parameter file. Each filter must be prefixed with either a + or a -, and multiple filters must be joined with {3}", "replace-filter", "append-filter", "prepend-filter", System.IO.Path.PathSeparator); } } + public static string FailedToParseParametersFileError(string path, string message) { return LC.L(@"Unable to read the parameters file ""{0}"", reason: {1}", path, message); } + public static string SkippingSourceArgumentsOnNonBackupOperation { get { return @"The --source argument was specified in the parameter file, but the current operation is not a backup operation, so the argument is ignored"; } } + public static string LogfileCommandDescription { get { return LC.L(@"Outputs log information to the file given"); } } + public static string LoglevelCommandDescription { get { return LC.L(@"Determines the amount of information written in the log file"); } } + public static string PortablemodeCommandDescription { get { return LC.L(@"Activates portable mode where the database is placed below the program executable"); } } + public static string SeriousError(string message) { return LC.L(@"A serious error occurred in Duplicati: {0}", message); } + public static string StartupFailure(System.Exception error) { return LC.L(@"Unable to start up, perhaps another process is already running? +Error message: {0}", error); } + public static string UnencrypteddatabaseCommandDescription { get { return LC.L(@"Disables database encryption"); } } + public static string WrongSQLiteVersion(System.Version actualversion, string expectedversion) { return LC.L(@"Unsupported version of SQLite detected ({0}), must be {1} or higher", actualversion, expectedversion); } + public static string WebserverWebrootDescription { get { return LC.L(@"The path to the folder where the static files for the webserver is present. The folder must be located beneath the installation folder"); } } + public static string WebserverPortDescription { get { return LC.L(@"The port the webserver listens on. Multiple values may be supplied with a comma in between."); } } + public static string WebserverCertificateFileDescription { get { return LC.L(@"The certificate and key file in PKCS #12 format the webserver use for SSL. Only RSA/DSA keys are supported."); } } + public static string WebserverCertificatePasswordDescription { get { return LC.L(@"The password for decryption of certificate PKCS #12 file."); } } + public static string WebserverInterfaceDescription { get { return LC.L(@"The interface the webserver listens on. The special values ""*"" and ""any"" means any interface. The special value ""loopback"" means the loopback adapter."); } } + public static string WebserverPasswordDescription { get { return LC.L(@"The password required to access the webserver. This option is saved so you do not need to set it on each run. Setting an empty value disables the password."); } } + public static string WebserverAllowedhostnamesDescription { get { return LC.L(@"The hostnames that are accepted, separated with semicolons. If any of the hostnames are ""*"", all hostnames are allowed and the hostname checking is disabled."); } } + public static string PingpongkeepaliveShort { get { return LC.L(@"Enables the ping-pong responder"); } } + public static string PingpongkeepaliveLong { get { return LC.L(@"When running as a server, the service daemon must verify that the process is responding. If this option is enabled, the server reads stdin and writes a reply to each line read"); } } + public static string LogretentionShort { get { return LC.L(@"Clean up old log data"); } } + public static string LogretentionLong { get { return LC.L(@"Set the time after which log data will be purged from the database."); } } + public static string ServerdatafolderShort { get { return LC.L(@"Sets the folder where settings are stored"); } } + public static string ServerdatafolderLong(string envname) { return LC.L(@"Duplicati needs to store a small database with all settings. Use this option to choose where the settings are stored. This option can also be set with the environment variable {0}.", envname); } + public static string ServerencryptionkeyShort { get { return LC.L(@"Sets the database encryption key"); } } + public static string ServerencryptionkeyLong(string envname, string decryptionoption) { return LC.L(@"This option sets the encryption key used to scramble the local settings database. This option can also be set with the environment variable {0}. Use the option --{1} to disable the database scrambling.", envname, decryptionoption); } + public static string TempdirShort { get { return LC.L(@"Temporary storage folder"); } } + public static string TempdirLong { get { return LC.L(@"This option can be used to supply an alternative folder for temporary storage. By default the system default temporary folder is used. Note that also SQLite will put temporary files in this temporary folder."); } } +} + internal static class Scheduler { + public static string InvalidTimeSetupError(System.DateTime startdate, string interval, string alloweddays) { return LC.L(@"Unable to find a valid date, given the start date {0}, the repetition interval {1} and the allowed days {2}", startdate, interval, alloweddays); } + } + internal static class Server + { + public static string DefectSSLCertInDatabase { get { return @"Unable to create SSL certificate using data from database. Starting without SSL."; } } + public static string StartedServer(string ip, int port) { return LC.L(@"Server has started and is listening on {0}, port {1}", ip, port); } + public static string SSLCertificateFailure(string errormessage) { return LC.L(@"Unable to create SSL certificate using provided parameters. Exception detail: {0}", errormessage); } + public static string ServerStartFailure(IEnumerable portstried) { return LC.L(@"Unable to open a socket for listening, tried ports: {0}", string.Join(",", from n in (portstried ?? new int[0]) select n.ToString())); } + } + +} diff --git a/Duplicati/Server/UpdatePollThread.cs b/Duplicati.Library.RestAPI/UpdatePollThread.cs similarity index 79% rename from Duplicati/Server/UpdatePollThread.cs rename to Duplicati.Library.RestAPI/UpdatePollThread.cs index 5a3e546ae..c82bd8d2d 100644 --- a/Duplicati/Server/UpdatePollThread.cs +++ b/Duplicati.Library.RestAPI/UpdatePollThread.cs @@ -1,236 +1,237 @@ -// Copyright (C) 2015, The Duplicati Team - -// http://www.duplicati.com, info@duplicati.com -// -// This library is free software; you can redistribute it and/or modify -// it under the terms of the GNU Lesser General Public License as -// published by the Free Software Foundation; either version 2.1 of the -// License, or (at your option) any later version. -// -// This library is distributed in the hope that it will be useful, but -// WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU -// Lesser General Public License for more details. -// -// You should have received a copy of the GNU Lesser General Public -// License along with this library; if not, write to the Free Software -// Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA -using System; -using System.Linq; -using System.Threading; -using Duplicati.Server.Serialization; - -namespace Duplicati.Server -{ - /// - /// The thread that checks on the update server if new versions are available - /// - public class UpdatePollThread - { - private readonly Thread m_thread; - private volatile bool m_terminated = false; - private volatile bool m_download = false; - private volatile bool m_forceCheck = false; - private readonly object m_lock = new object(); - private readonly AutoResetEvent m_waitSignal; - private double m_downloadProgress; - - public bool IsUpdateRequested { get; private set; } = false; - - public UpdatePollerStates ThreadState { get; private set; } - public double DownloadProgess - { - get { return m_downloadProgress ; } - - private set - { - var oldv = m_downloadProgress; - m_downloadProgress = value; - if ((int)(oldv * 100) != (int)(value * 100)) - Program.StatusEventNotifyer.SignalNewEvent(); - } - } - - public UpdatePollThread() - { - m_waitSignal = new AutoResetEvent(false); - ThreadState = UpdatePollerStates.Waiting; - m_thread = new Thread(Run); - m_thread.IsBackground = true; - m_thread.Name = "UpdatePollThread"; - m_thread.Start(); - } - - public void CheckNow() - { - lock(m_lock) - { - m_forceCheck = true; - m_waitSignal.Set(); - } - } - - public void InstallUpdate() - { - lock(m_lock) - { - m_forceCheck = true; - m_download = true; - m_waitSignal.Set(); - } - } - - public void ActivateUpdate() - { - if (Duplicati.Library.AutoUpdater.UpdaterManager.SetRunUpdate()) - { - IsUpdateRequested = true; - Program.ApplicationExitEvent.Set(); - } - } - - public void Terminate() - { - lock(m_lock) - { - m_terminated = true; - m_waitSignal.Set(); - } - } - - public void Reschedule() - { - m_waitSignal.Set(); - } - - private void Run() - { - // Wait on startup - m_waitSignal.WaitOne(TimeSpan.FromMinutes(1), true); - - while (!m_terminated) - { - var nextCheck = Program.DataConnection.ApplicationSettings.NextUpdateCheck; - - var maxcheck = TimeSpan.FromDays(7); - try - { - maxcheck = Library.Utility.Timeparser.ParseTimeSpan(Program.DataConnection.ApplicationSettings.UpdateCheckInterval); - } - catch - { - } - - // If we have some weirdness, just check now - if (nextCheck - DateTime.UtcNow > maxcheck) - nextCheck = DateTime.UtcNow - TimeSpan.FromSeconds(1); - - if (nextCheck < DateTime.UtcNow || m_forceCheck) - { - lock(m_lock) - m_forceCheck = false; - - ThreadState = UpdatePollerStates.Checking; - Program.StatusEventNotifyer.SignalNewEvent(); - - DateTime started = DateTime.UtcNow; - Program.DataConnection.ApplicationSettings.LastUpdateCheck = started; - nextCheck = Program.DataConnection.ApplicationSettings.NextUpdateCheck; - - Library.AutoUpdater.ReleaseType rt; - if (!Enum.TryParse(Program.DataConnection.ApplicationSettings.UpdateChannel, true, out rt)) - rt = Duplicati.Library.AutoUpdater.ReleaseType.Unknown; - - // Choose the default channel in case we have unknown - rt = rt == Duplicati.Library.AutoUpdater.ReleaseType.Unknown ? Duplicati.Library.AutoUpdater.AutoUpdateSettings.DefaultUpdateChannel : rt; - - try - { - var update = Duplicati.Library.AutoUpdater.UpdaterManager.CheckForUpdate(rt); - if (update != null) - Program.DataConnection.ApplicationSettings.UpdatedVersion = update; - } - catch - { - } - - // It could be that we have registered an update from a more unstable channel, - // but the user has switched to a more stable channel. - // In that case we discard the old update to avoid offering it. - if (Program.DataConnection.ApplicationSettings.UpdatedVersion != null) - { - Library.AutoUpdater.ReleaseType updatert; - var updatertstring = Program.DataConnection.ApplicationSettings.UpdatedVersion.ReleaseType; - if (string.Equals(updatertstring, "preview", StringComparison.OrdinalIgnoreCase)) - updatertstring = Library.AutoUpdater.ReleaseType.Experimental.ToString(); - - if (!Enum.TryParse(updatertstring, true, out updatert)) - updatert = Duplicati.Library.AutoUpdater.ReleaseType.Nightly; - - if (updatert == Duplicati.Library.AutoUpdater.ReleaseType.Unknown) - updatert = Duplicati.Library.AutoUpdater.ReleaseType.Nightly; - - if (updatert > rt) - Program.DataConnection.ApplicationSettings.UpdatedVersion = null; - } - - if (Program.DataConnection.ApplicationSettings.UpdatedVersion != null && Duplicati.Library.AutoUpdater.UpdaterManager.TryParseVersion(Program.DataConnection.ApplicationSettings.UpdatedVersion.Version) > System.Reflection.Assembly.GetExecutingAssembly().GetName().Version) - { - Program.DataConnection.RegisterNotification( - NotificationType.Information, - "Found update", - Program.DataConnection.ApplicationSettings.UpdatedVersion.Displayname, - null, - null, - "update:new", - null, - "NewUpdateFound", - null, - (self, all) => { - return all.FirstOrDefault(x => x.Action == "update:new") ?? self; - } - ); - } - } - - if (m_download) - { - lock(m_lock) - m_download = false; - - var v = Program.DataConnection.ApplicationSettings.UpdatedVersion; - if (v != null) - { - ThreadState = UpdatePollerStates.Downloading; - Program.StatusEventNotifyer.SignalNewEvent(); - - if (Duplicati.Library.AutoUpdater.UpdaterManager.DownloadAndUnpackUpdate(v, (pg) => { DownloadProgess = pg; })) - Program.StatusEventNotifyer.SignalNewEvent(); - } - } - - DownloadProgess = 0; - - if (ThreadState != UpdatePollerStates.Waiting) - { - ThreadState = UpdatePollerStates.Waiting; - Program.StatusEventNotifyer.SignalNewEvent(); - } - - var waitTime = nextCheck - DateTime.UtcNow; - - // Guard against spin-loop - if (waitTime.TotalSeconds < 5) - waitTime = TimeSpan.FromSeconds(5); - - // Guard against year-long waits - // A re-check does not cause an update check - if (waitTime.TotalDays > 1) - waitTime = TimeSpan.FromDays(1); - - m_waitSignal.WaitOne(waitTime, true); - } - } - } -} - +// Copyright (C) 2015, The Duplicati Team + +// http://www.duplicati.com, info@duplicati.com +// +// This library is free software; you can redistribute it and/or modify +// it under the terms of the GNU Lesser General Public License as +// published by the Free Software Foundation; either version 2.1 of the +// License, or (at your option) any later version. +// +// This library is distributed in the hope that it will be useful, but +// WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +// Lesser General Public License for more details. +// +// You should have received a copy of the GNU Lesser General Public +// License along with this library; if not, write to the Free Software +// Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA +using System; +using System.Linq; +using System.Threading; +using Duplicati.Library.RestAPI; +using Duplicati.Server.Serialization; + +namespace Duplicati.Server +{ + /// + /// The thread that checks on the update server if new versions are available + /// + public class UpdatePollThread + { + private readonly Thread m_thread; + private volatile bool m_terminated = false; + private volatile bool m_download = false; + private volatile bool m_forceCheck = false; + private readonly object m_lock = new object(); + private readonly AutoResetEvent m_waitSignal; + private double m_downloadProgress; + + public bool IsUpdateRequested { get; private set; } = false; + + public UpdatePollerStates ThreadState { get; private set; } + public double DownloadProgess + { + get { return m_downloadProgress ; } + + private set + { + var oldv = m_downloadProgress; + m_downloadProgress = value; + if ((int)(oldv * 100) != (int)(value * 100)) + FIXMEGlobal.StatusEventNotifyer.SignalNewEvent(); + } + } + + public UpdatePollThread() + { + m_waitSignal = new AutoResetEvent(false); + ThreadState = UpdatePollerStates.Waiting; + m_thread = new Thread(Run); + m_thread.IsBackground = true; + m_thread.Name = "UpdatePollThread"; + m_thread.Start(); + } + + public void CheckNow() + { + lock(m_lock) + { + m_forceCheck = true; + m_waitSignal.Set(); + } + } + + public void InstallUpdate() + { + lock(m_lock) + { + m_forceCheck = true; + m_download = true; + m_waitSignal.Set(); + } + } + + public void ActivateUpdate() + { + if (Duplicati.Library.AutoUpdater.UpdaterManager.SetRunUpdate()) + { + IsUpdateRequested = true; + FIXMEGlobal.ApplicationExitEvent.Set(); + } + } + + public void Terminate() + { + lock(m_lock) + { + m_terminated = true; + m_waitSignal.Set(); + } + } + + public void Reschedule() + { + m_waitSignal.Set(); + } + + private void Run() + { + // Wait on startup + m_waitSignal.WaitOne(TimeSpan.FromMinutes(1), true); + + while (!m_terminated) + { + var nextCheck = FIXMEGlobal.DataConnection.ApplicationSettings.NextUpdateCheck; + + var maxcheck = TimeSpan.FromDays(7); + try + { + maxcheck = Library.Utility.Timeparser.ParseTimeSpan(FIXMEGlobal.DataConnection.ApplicationSettings.UpdateCheckInterval); + } + catch + { + } + + // If we have some weirdness, just check now + if (nextCheck - DateTime.UtcNow > maxcheck) + nextCheck = DateTime.UtcNow - TimeSpan.FromSeconds(1); + + if (nextCheck < DateTime.UtcNow || m_forceCheck) + { + lock(m_lock) + m_forceCheck = false; + + ThreadState = UpdatePollerStates.Checking; + FIXMEGlobal.StatusEventNotifyer.SignalNewEvent(); + + DateTime started = DateTime.UtcNow; + FIXMEGlobal.DataConnection.ApplicationSettings.LastUpdateCheck = started; + nextCheck = FIXMEGlobal.DataConnection.ApplicationSettings.NextUpdateCheck; + + Library.AutoUpdater.ReleaseType rt; + if (!Enum.TryParse(FIXMEGlobal.DataConnection.ApplicationSettings.UpdateChannel, true, out rt)) + rt = Duplicati.Library.AutoUpdater.ReleaseType.Unknown; + + // Choose the default channel in case we have unknown + rt = rt == Duplicati.Library.AutoUpdater.ReleaseType.Unknown ? Duplicati.Library.AutoUpdater.AutoUpdateSettings.DefaultUpdateChannel : rt; + + try + { + var update = Duplicati.Library.AutoUpdater.UpdaterManager.CheckForUpdate(rt); + if (update != null) + FIXMEGlobal.DataConnection.ApplicationSettings.UpdatedVersion = update; + } + catch + { + } + + // It could be that we have registered an update from a more unstable channel, + // but the user has switched to a more stable channel. + // In that case we discard the old update to avoid offering it. + if (FIXMEGlobal.DataConnection.ApplicationSettings.UpdatedVersion != null) + { + Library.AutoUpdater.ReleaseType updatert; + var updatertstring = FIXMEGlobal.DataConnection.ApplicationSettings.UpdatedVersion.ReleaseType; + if (string.Equals(updatertstring, "preview", StringComparison.OrdinalIgnoreCase)) + updatertstring = Library.AutoUpdater.ReleaseType.Experimental.ToString(); + + if (!Enum.TryParse(updatertstring, true, out updatert)) + updatert = Duplicati.Library.AutoUpdater.ReleaseType.Nightly; + + if (updatert == Duplicati.Library.AutoUpdater.ReleaseType.Unknown) + updatert = Duplicati.Library.AutoUpdater.ReleaseType.Nightly; + + if (updatert > rt) + FIXMEGlobal.DataConnection.ApplicationSettings.UpdatedVersion = null; + } + + if (FIXMEGlobal.DataConnection.ApplicationSettings.UpdatedVersion != null && Duplicati.Library.AutoUpdater.UpdaterManager.TryParseVersion(FIXMEGlobal.DataConnection.ApplicationSettings.UpdatedVersion.Version) > System.Reflection.Assembly.GetExecutingAssembly().GetName().Version) + { + FIXMEGlobal.DataConnection.RegisterNotification( + NotificationType.Information, + "Found update", + FIXMEGlobal.DataConnection.ApplicationSettings.UpdatedVersion.Displayname, + null, + null, + "update:new", + null, + "NewUpdateFound", + null, + (self, all) => { + return all.FirstOrDefault(x => x.Action == "update:new") ?? self; + } + ); + } + } + + if (m_download) + { + lock(m_lock) + m_download = false; + + var v = FIXMEGlobal.DataConnection.ApplicationSettings.UpdatedVersion; + if (v != null) + { + ThreadState = UpdatePollerStates.Downloading; + FIXMEGlobal.StatusEventNotifyer.SignalNewEvent(); + + if (Duplicati.Library.AutoUpdater.UpdaterManager.DownloadAndUnpackUpdate(v, (pg) => { DownloadProgess = pg; })) + FIXMEGlobal.StatusEventNotifyer.SignalNewEvent(); + } + } + + DownloadProgess = 0; + + if (ThreadState != UpdatePollerStates.Waiting) + { + ThreadState = UpdatePollerStates.Waiting; + FIXMEGlobal.StatusEventNotifyer.SignalNewEvent(); + } + + var waitTime = nextCheck - DateTime.UtcNow; + + // Guard against spin-loop + if (waitTime.TotalSeconds < 5) + waitTime = TimeSpan.FromSeconds(5); + + // Guard against year-long waits + // A re-check does not cause an update check + if (waitTime.TotalDays > 1) + waitTime = TimeSpan.FromDays(1); + + m_waitSignal.WaitOne(waitTime, true); + } + } + } +} + diff --git a/Duplicati/Server/WebServer/AuthenticationHandler.cs b/Duplicati.Library.RestAPI/WebServer/AuthenticationHandler.cs similarity index 95% rename from Duplicati/Server/WebServer/AuthenticationHandler.cs rename to Duplicati.Library.RestAPI/WebServer/AuthenticationHandler.cs index 42537f501..3dd26011d 100644 --- a/Duplicati/Server/WebServer/AuthenticationHandler.cs +++ b/Duplicati.Library.RestAPI/WebServer/AuthenticationHandler.cs @@ -1,338 +1,339 @@ -// Copyright (C) 2015, The Duplicati Team - -// http://www.duplicati.com, info@duplicati.com -// -// This library is free software; you can redistribute it and/or modify -// it under the terms of the GNU Lesser General Public License as -// published by the Free Software Foundation; either version 2.1 of the -// License, or (at your option) any later version. -// -// This library is distributed in the hope that it will be useful, but -// WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU -// Lesser General Public License for more details. -// -// You should have received a copy of the GNU Lesser General Public -// License along with this library; if not, write to the Free Software -// Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA -using System; -using System.Collections.Concurrent; -using System.Linq; -using HttpServer; -using HttpServer.HttpModules; -using System.Collections.Generic; - -namespace Duplicati.Server.WebServer -{ - internal class AuthenticationHandler : HttpModule - { - private const string AUTH_COOKIE_NAME = "session-auth"; - private const string NONCE_COOKIE_NAME = "session-nonce"; - - private const string XSRF_COOKIE_NAME = "xsrf-token"; - private const string XSRF_HEADER_NAME = "X-XSRF-Token"; - - private const string TRAYICONPASSWORDSOURCE_HEADER = "X-TrayIcon-PasswordSource"; - - public const string LOGIN_SCRIPT_URI = "/login.cgi"; - public const string LOGOUT_SCRIPT_URI = "/logout.cgi"; - public const string CAPTCHA_IMAGE_URI = RESTHandler.API_URI_PATH + "/captcha/"; - - private const int XSRF_TIMEOUT_MINUTES = 10; - private const int AUTH_TIMEOUT_MINUTES = 10; - - private readonly ConcurrentDictionary m_activeTokens = new ConcurrentDictionary(); - private readonly ConcurrentDictionary> m_activeNonces = new ConcurrentDictionary>(); - private readonly ConcurrentDictionary m_activexsrf = new ConcurrentDictionary(); - - readonly System.Security.Cryptography.RandomNumberGenerator m_prng = System.Security.Cryptography.RNGCryptoServiceProvider.Create(); - - private string FindXSRFToken(HttpServer.IHttpRequest request) - { - string xsrftoken = request.Headers[XSRF_HEADER_NAME] ?? ""; - - if (string.IsNullOrWhiteSpace(xsrftoken)) - { - var xsrfq = request.Form[XSRF_HEADER_NAME] ?? request.Form[Duplicati.Library.Utility.Uri.UrlEncode(XSRF_HEADER_NAME)]; - xsrftoken = (xsrfq == null || string.IsNullOrWhiteSpace(xsrfq.Value)) ? "" : xsrfq.Value; - } - - if (string.IsNullOrWhiteSpace(xsrftoken)) - { - var xsrfq = request.QueryString[XSRF_HEADER_NAME] ?? request.QueryString[Duplicati.Library.Utility.Uri.UrlEncode(XSRF_HEADER_NAME)]; - xsrftoken = (xsrfq == null || string.IsNullOrWhiteSpace(xsrfq.Value)) ? "" : xsrfq.Value; - } - - return xsrftoken; - } - - private bool AddXSRFTokenToRespone(HttpServer.IHttpResponse response) - { - if (m_activexsrf.Count > 500) - return false; - - var buf = new byte[32]; - var expires = DateTime.UtcNow.AddMinutes(XSRF_TIMEOUT_MINUTES); - m_prng.GetBytes(buf); - var token = Convert.ToBase64String(buf); - - m_activexsrf.AddOrUpdate(token, key => expires, (key, existingExpires) => - { - // Simulate the original behavior => if the random token, against all odds, is already used - // we throw an ArgumentException - throw new ArgumentException("An element with the same key already exists in the dictionary."); - }); - - response.Cookies.Add(new HttpServer.ResponseCookie(XSRF_COOKIE_NAME, token, expires)); - return true; - } - - private string FindAuthCookie(HttpServer.IHttpRequest request) - { - var authcookie = request.Cookies[AUTH_COOKIE_NAME] ?? request.Cookies[Library.Utility.Uri.UrlEncode(AUTH_COOKIE_NAME)]; - var authform = request.Form["auth-token"] ?? request.Form[Library.Utility.Uri.UrlEncode("auth-token")]; - var authquery = request.QueryString["auth-token"] ?? request.QueryString[Library.Utility.Uri.UrlEncode("auth-token")]; - - var auth_token = string.IsNullOrWhiteSpace(authcookie?.Value) ? null : authcookie.Value; - if (!string.IsNullOrWhiteSpace(authquery?.Value)) - auth_token = authquery.Value; - if (!string.IsNullOrWhiteSpace(authform?.Value)) - auth_token = authform.Value; - - return auth_token; - } - - private bool HasXSRFCookie(HttpServer.IHttpRequest request) - { - // Clean up expired XSRF cookies - foreach (var k in (from n in m_activexsrf where DateTime.UtcNow > n.Value select n.Key)) - m_activexsrf.TryRemove(k, out _); - - var xsrfcookie = request.Cookies[XSRF_COOKIE_NAME] ?? request.Cookies[Library.Utility.Uri.UrlEncode(XSRF_COOKIE_NAME)]; - var value = xsrfcookie == null ? null : xsrfcookie.Value; - if (string.IsNullOrWhiteSpace(value)) - return false; - - if (m_activexsrf.ContainsKey(value)) - { - m_activexsrf[value] = DateTime.UtcNow.AddMinutes(XSRF_TIMEOUT_MINUTES); - return true; - } - else if (m_activexsrf.ContainsKey(Library.Utility.Uri.UrlDecode(value))) - { - m_activexsrf[Library.Utility.Uri.UrlDecode(value)] = DateTime.UtcNow.AddMinutes(XSRF_TIMEOUT_MINUTES); - return true; - } - - return false; - } - - public override bool Process(HttpServer.IHttpRequest request, HttpServer.IHttpResponse response, HttpServer.Sessions.IHttpSession session) - { - HttpServer.HttpInput input = String.Equals(request.Method, "POST", StringComparison.OrdinalIgnoreCase) ? request.Form : request.QueryString; - - var auth_token = FindAuthCookie(request); - var xsrf_token = FindXSRFToken(request); - - if (!HasXSRFCookie(request)) - { - var cookieAdded = AddXSRFTokenToRespone(response); - - if (!cookieAdded) - { - response.Status = System.Net.HttpStatusCode.ServiceUnavailable; - response.Reason = "Too Many Concurrent Request, try again later"; - return true; - } - } - - if (LOGOUT_SCRIPT_URI.Equals(request.Uri.AbsolutePath, StringComparison.OrdinalIgnoreCase)) - { - if (!string.IsNullOrWhiteSpace(auth_token)) - { - // Remove the active auth token - m_activeTokens.TryRemove(auth_token, out _); - } - - response.Status = System.Net.HttpStatusCode.NoContent; - response.Reason = "OK"; - - return true; - } - else if (LOGIN_SCRIPT_URI.Equals(request.Uri.AbsolutePath, StringComparison.OrdinalIgnoreCase)) - { - // Remove expired nonces - foreach(var k in (from n in m_activeNonces where DateTime.UtcNow > n.Value.Item1 select n.Key)) - m_activeNonces.TryRemove(k, out _); - - if (input["get-nonce"] != null && !string.IsNullOrWhiteSpace(input["get-nonce"].Value)) - { - if (m_activeNonces.Count > 50) - { - response.Status = System.Net.HttpStatusCode.ServiceUnavailable; - response.Reason = "Too many active login attempts"; - return true; - } - - var password = Program.DataConnection.ApplicationSettings.WebserverPassword; - - if (request.Headers[TRAYICONPASSWORDSOURCE_HEADER] == "database") - password = Program.DataConnection.ApplicationSettings.WebserverPasswordTrayIconHash; - - var buf = new byte[32]; - var expires = DateTime.UtcNow.AddMinutes(AUTH_TIMEOUT_MINUTES); - m_prng.GetBytes(buf); - var nonce = Convert.ToBase64String(buf); - - var sha256 = System.Security.Cryptography.SHA256.Create(); - sha256.TransformBlock(buf, 0, buf.Length, buf, 0); - buf = Convert.FromBase64String(password); - sha256.TransformFinalBlock(buf, 0, buf.Length); - var pwd = Convert.ToBase64String(sha256.Hash); - - m_activeNonces.AddOrUpdate(nonce, key => new Tuple(expires, pwd), (key, existingValue) => - { - // Simulate the original behavior => if the nonce, against all odds, is already used - // we throw an ArgumentException - throw new ArgumentException("An element with the same key already exists in the dictionary."); - }); - - response.Cookies.Add(new HttpServer.ResponseCookie(NONCE_COOKIE_NAME, nonce, expires)); - using(var bw = new BodyWriter(response, request)) - { - bw.OutputOK(new { - Status = "OK", - Nonce = nonce, - Salt = Program.DataConnection.ApplicationSettings.WebserverPasswordSalt - }); - } - return true; - } - else - { - if (input["password"] != null && !string.IsNullOrWhiteSpace(input["password"].Value)) - { - var nonce_el = request.Cookies[NONCE_COOKIE_NAME] ?? request.Cookies[Library.Utility.Uri.UrlEncode(NONCE_COOKIE_NAME)]; - var nonce = nonce_el == null || string.IsNullOrWhiteSpace(nonce_el.Value) ? "" : nonce_el.Value; - var urldecoded = nonce == null ? "" : Duplicati.Library.Utility.Uri.UrlDecode(nonce); - if (m_activeNonces.ContainsKey(urldecoded)) - nonce = urldecoded; - - if (!m_activeNonces.ContainsKey(nonce)) - { - response.Status = System.Net.HttpStatusCode.Unauthorized; - response.Reason = "Unauthorized"; - response.ContentType = "application/json"; - return true; - } - - var pwd = m_activeNonces[nonce].Item2; - - // Remove the nonce - m_activeNonces.TryRemove(nonce, out _); - - if (pwd != input["password"].Value) - { - response.Status = System.Net.HttpStatusCode.Unauthorized; - response.Reason = "Unauthorized"; - response.ContentType = "application/json"; - return true; - } - - var buf = new byte[32]; - var expires = DateTime.UtcNow.AddHours(1); - m_prng.GetBytes(buf); - var token = Duplicati.Library.Utility.Utility.Base64UrlEncode(buf); - while (token.Length > 0 && token.EndsWith("=", StringComparison.Ordinal)) - token = token.Substring(0, token.Length - 1); - - m_activeTokens.AddOrUpdate(token, key => expires, (key, existingValue) => - { - // Simulate the original behavior => if the token, against all odds, is already used - // we throw an ArgumentException - throw new ArgumentException("An element with the same key already exists in the dictionary."); - }); - - response.Cookies.Add(new HttpServer.ResponseCookie(AUTH_COOKIE_NAME, token, expires)); - - using(var bw = new BodyWriter(response, request)) - bw.OutputOK(); - - return true; - } - } - } - - var limitedAccess = - request.Uri.AbsolutePath.StartsWith(RESTHandler.API_URI_PATH, StringComparison.OrdinalIgnoreCase) - ; - - // Override to allow the CAPTCHA call to go through - if (request.Uri.AbsolutePath.StartsWith(CAPTCHA_IMAGE_URI, StringComparison.OrdinalIgnoreCase) && request.Method == "GET") - limitedAccess = false; - - if (limitedAccess) - { - if (xsrf_token != null && m_activexsrf.ContainsKey(xsrf_token)) - { - var expires = DateTime.UtcNow.AddMinutes(XSRF_TIMEOUT_MINUTES); - m_activexsrf[xsrf_token] = expires; - response.Cookies.Add(new ResponseCookie(XSRF_COOKIE_NAME, xsrf_token, expires)); - } - else - { - response.Status = System.Net.HttpStatusCode.BadRequest; - response.Reason = "Missing XSRF Token. Please reload the page"; - - return true; - } - } - - if (string.IsNullOrWhiteSpace(Program.DataConnection.ApplicationSettings.WebserverPassword)) - return false; - - foreach(var k in (from n in m_activeTokens where DateTime.UtcNow > n.Value select n.Key)) - m_activeTokens.TryRemove(k, out _); - - - // If we have a valid token, proceed - if (!string.IsNullOrWhiteSpace(auth_token)) - { - DateTime expires; - var found = m_activeTokens.TryGetValue(auth_token, out expires); - if (!found) - { - auth_token = Duplicati.Library.Utility.Uri.UrlDecode(auth_token); - found = m_activeTokens.TryGetValue(auth_token, out expires); - } - - if (found && DateTime.UtcNow < expires) - { - expires = DateTime.UtcNow.AddHours(1); - - m_activeTokens[auth_token] = expires; - response.Cookies.Add(new ResponseCookie(AUTH_COOKIE_NAME, auth_token, expires)); - return false; - } - } - - if ("/".Equals(request.Uri.AbsolutePath, StringComparison.OrdinalIgnoreCase) || "/index.html".Equals(request.Uri.AbsolutePath, StringComparison.OrdinalIgnoreCase)) - { - response.Redirect("/login.html"); - return true; - } - - if (limitedAccess) - { - response.Status = System.Net.HttpStatusCode.Unauthorized; - response.Reason = "Not logged in"; - response.AddHeader("Location", "login.html"); - - return true; - } - - return false; - } - } -} - +// Copyright (C) 2015, The Duplicati Team + +// http://www.duplicati.com, info@duplicati.com +// +// This library is free software; you can redistribute it and/or modify +// it under the terms of the GNU Lesser General Public License as +// published by the Free Software Foundation; either version 2.1 of the +// License, or (at your option) any later version. +// +// This library is distributed in the hope that it will be useful, but +// WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +// Lesser General Public License for more details. +// +// You should have received a copy of the GNU Lesser General Public +// License along with this library; if not, write to the Free Software +// Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA +using System; +using System.Collections.Concurrent; +using System.Linq; +using HttpServer; +using HttpServer.HttpModules; +using System.Collections.Generic; +using Duplicati.Library.RestAPI; + +namespace Duplicati.Server.WebServer +{ + internal class AuthenticationHandler : HttpModule + { + private const string AUTH_COOKIE_NAME = "session-auth"; + private const string NONCE_COOKIE_NAME = "session-nonce"; + + private const string XSRF_COOKIE_NAME = "xsrf-token"; + private const string XSRF_HEADER_NAME = "X-XSRF-Token"; + + private const string TRAYICONPASSWORDSOURCE_HEADER = "X-TrayIcon-PasswordSource"; + + public const string LOGIN_SCRIPT_URI = "/login.cgi"; + public const string LOGOUT_SCRIPT_URI = "/logout.cgi"; + public const string CAPTCHA_IMAGE_URI = RESTHandler.API_URI_PATH + "/captcha/"; + + private const int XSRF_TIMEOUT_MINUTES = 10; + private const int AUTH_TIMEOUT_MINUTES = 10; + + private readonly ConcurrentDictionary m_activeTokens = new ConcurrentDictionary(); + private readonly ConcurrentDictionary> m_activeNonces = new ConcurrentDictionary>(); + private readonly ConcurrentDictionary m_activexsrf = new ConcurrentDictionary(); + + readonly System.Security.Cryptography.RandomNumberGenerator m_prng = System.Security.Cryptography.RNGCryptoServiceProvider.Create(); + + private string FindXSRFToken(HttpServer.IHttpRequest request) + { + string xsrftoken = request.Headers[XSRF_HEADER_NAME] ?? ""; + + if (string.IsNullOrWhiteSpace(xsrftoken)) + { + var xsrfq = request.Form[XSRF_HEADER_NAME] ?? request.Form[Duplicati.Library.Utility.Uri.UrlEncode(XSRF_HEADER_NAME)]; + xsrftoken = (xsrfq == null || string.IsNullOrWhiteSpace(xsrfq.Value)) ? "" : xsrfq.Value; + } + + if (string.IsNullOrWhiteSpace(xsrftoken)) + { + var xsrfq = request.QueryString[XSRF_HEADER_NAME] ?? request.QueryString[Duplicati.Library.Utility.Uri.UrlEncode(XSRF_HEADER_NAME)]; + xsrftoken = (xsrfq == null || string.IsNullOrWhiteSpace(xsrfq.Value)) ? "" : xsrfq.Value; + } + + return xsrftoken; + } + + private bool AddXSRFTokenToRespone(HttpServer.IHttpResponse response) + { + if (m_activexsrf.Count > 500) + return false; + + var buf = new byte[32]; + var expires = DateTime.UtcNow.AddMinutes(XSRF_TIMEOUT_MINUTES); + m_prng.GetBytes(buf); + var token = Convert.ToBase64String(buf); + + m_activexsrf.AddOrUpdate(token, key => expires, (key, existingExpires) => + { + // Simulate the original behavior => if the random token, against all odds, is already used + // we throw an ArgumentException + throw new ArgumentException("An element with the same key already exists in the dictionary."); + }); + + response.Cookies.Add(new HttpServer.ResponseCookie(XSRF_COOKIE_NAME, token, expires)); + return true; + } + + private string FindAuthCookie(HttpServer.IHttpRequest request) + { + var authcookie = request.Cookies[AUTH_COOKIE_NAME] ?? request.Cookies[Library.Utility.Uri.UrlEncode(AUTH_COOKIE_NAME)]; + var authform = request.Form["auth-token"] ?? request.Form[Library.Utility.Uri.UrlEncode("auth-token")]; + var authquery = request.QueryString["auth-token"] ?? request.QueryString[Library.Utility.Uri.UrlEncode("auth-token")]; + + var auth_token = string.IsNullOrWhiteSpace(authcookie?.Value) ? null : authcookie.Value; + if (!string.IsNullOrWhiteSpace(authquery?.Value)) + auth_token = authquery.Value; + if (!string.IsNullOrWhiteSpace(authform?.Value)) + auth_token = authform.Value; + + return auth_token; + } + + private bool HasXSRFCookie(HttpServer.IHttpRequest request) + { + // Clean up expired XSRF cookies + foreach (var k in (from n in m_activexsrf where DateTime.UtcNow > n.Value select n.Key)) + m_activexsrf.TryRemove(k, out _); + + var xsrfcookie = request.Cookies[XSRF_COOKIE_NAME] ?? request.Cookies[Library.Utility.Uri.UrlEncode(XSRF_COOKIE_NAME)]; + var value = xsrfcookie == null ? null : xsrfcookie.Value; + if (string.IsNullOrWhiteSpace(value)) + return false; + + if (m_activexsrf.ContainsKey(value)) + { + m_activexsrf[value] = DateTime.UtcNow.AddMinutes(XSRF_TIMEOUT_MINUTES); + return true; + } + else if (m_activexsrf.ContainsKey(Library.Utility.Uri.UrlDecode(value))) + { + m_activexsrf[Library.Utility.Uri.UrlDecode(value)] = DateTime.UtcNow.AddMinutes(XSRF_TIMEOUT_MINUTES); + return true; + } + + return false; + } + + public override bool Process(HttpServer.IHttpRequest request, HttpServer.IHttpResponse response, HttpServer.Sessions.IHttpSession session) + { + HttpServer.HttpInput input = String.Equals(request.Method, "POST", StringComparison.OrdinalIgnoreCase) ? request.Form : request.QueryString; + + var auth_token = FindAuthCookie(request); + var xsrf_token = FindXSRFToken(request); + + if (!HasXSRFCookie(request)) + { + var cookieAdded = AddXSRFTokenToRespone(response); + + if (!cookieAdded) + { + response.Status = System.Net.HttpStatusCode.ServiceUnavailable; + response.Reason = "Too Many Concurrent Request, try again later"; + return true; + } + } + + if (LOGOUT_SCRIPT_URI.Equals(request.Uri.AbsolutePath, StringComparison.OrdinalIgnoreCase)) + { + if (!string.IsNullOrWhiteSpace(auth_token)) + { + // Remove the active auth token + m_activeTokens.TryRemove(auth_token, out _); + } + + response.Status = System.Net.HttpStatusCode.NoContent; + response.Reason = "OK"; + + return true; + } + else if (LOGIN_SCRIPT_URI.Equals(request.Uri.AbsolutePath, StringComparison.OrdinalIgnoreCase)) + { + // Remove expired nonces + foreach(var k in (from n in m_activeNonces where DateTime.UtcNow > n.Value.Item1 select n.Key)) + m_activeNonces.TryRemove(k, out _); + + if (input["get-nonce"] != null && !string.IsNullOrWhiteSpace(input["get-nonce"].Value)) + { + if (m_activeNonces.Count > 50) + { + response.Status = System.Net.HttpStatusCode.ServiceUnavailable; + response.Reason = "Too many active login attempts"; + return true; + } + + var password = FIXMEGlobal.DataConnection.ApplicationSettings.WebserverPassword; + + if (request.Headers[TRAYICONPASSWORDSOURCE_HEADER] == "database") + password = FIXMEGlobal.DataConnection.ApplicationSettings.WebserverPasswordTrayIconHash; + + var buf = new byte[32]; + var expires = DateTime.UtcNow.AddMinutes(AUTH_TIMEOUT_MINUTES); + m_prng.GetBytes(buf); + var nonce = Convert.ToBase64String(buf); + + var sha256 = System.Security.Cryptography.SHA256.Create(); + sha256.TransformBlock(buf, 0, buf.Length, buf, 0); + buf = Convert.FromBase64String(password); + sha256.TransformFinalBlock(buf, 0, buf.Length); + var pwd = Convert.ToBase64String(sha256.Hash); + + m_activeNonces.AddOrUpdate(nonce, key => new Tuple(expires, pwd), (key, existingValue) => + { + // Simulate the original behavior => if the nonce, against all odds, is already used + // we throw an ArgumentException + throw new ArgumentException("An element with the same key already exists in the dictionary."); + }); + + response.Cookies.Add(new HttpServer.ResponseCookie(NONCE_COOKIE_NAME, nonce, expires)); + using(var bw = new BodyWriter(response, request)) + { + bw.OutputOK(new { + Status = "OK", + Nonce = nonce, + Salt = FIXMEGlobal.DataConnection.ApplicationSettings.WebserverPasswordSalt + }); + } + return true; + } + else + { + if (input["password"] != null && !string.IsNullOrWhiteSpace(input["password"].Value)) + { + var nonce_el = request.Cookies[NONCE_COOKIE_NAME] ?? request.Cookies[Library.Utility.Uri.UrlEncode(NONCE_COOKIE_NAME)]; + var nonce = nonce_el == null || string.IsNullOrWhiteSpace(nonce_el.Value) ? "" : nonce_el.Value; + var urldecoded = nonce == null ? "" : Duplicati.Library.Utility.Uri.UrlDecode(nonce); + if (m_activeNonces.ContainsKey(urldecoded)) + nonce = urldecoded; + + if (!m_activeNonces.ContainsKey(nonce)) + { + response.Status = System.Net.HttpStatusCode.Unauthorized; + response.Reason = "Unauthorized"; + response.ContentType = "application/json"; + return true; + } + + var pwd = m_activeNonces[nonce].Item2; + + // Remove the nonce + m_activeNonces.TryRemove(nonce, out _); + + if (pwd != input["password"].Value) + { + response.Status = System.Net.HttpStatusCode.Unauthorized; + response.Reason = "Unauthorized"; + response.ContentType = "application/json"; + return true; + } + + var buf = new byte[32]; + var expires = DateTime.UtcNow.AddHours(1); + m_prng.GetBytes(buf); + var token = Duplicati.Library.Utility.Utility.Base64UrlEncode(buf); + while (token.Length > 0 && token.EndsWith("=", StringComparison.Ordinal)) + token = token.Substring(0, token.Length - 1); + + m_activeTokens.AddOrUpdate(token, key => expires, (key, existingValue) => + { + // Simulate the original behavior => if the token, against all odds, is already used + // we throw an ArgumentException + throw new ArgumentException("An element with the same key already exists in the dictionary."); + }); + + response.Cookies.Add(new HttpServer.ResponseCookie(AUTH_COOKIE_NAME, token, expires)); + + using(var bw = new BodyWriter(response, request)) + bw.OutputOK(); + + return true; + } + } + } + + var limitedAccess = + request.Uri.AbsolutePath.StartsWith(RESTHandler.API_URI_PATH, StringComparison.OrdinalIgnoreCase) + ; + + // Override to allow the CAPTCHA call to go through + if (request.Uri.AbsolutePath.StartsWith(CAPTCHA_IMAGE_URI, StringComparison.OrdinalIgnoreCase) && request.Method == "GET") + limitedAccess = false; + + if (limitedAccess) + { + if (xsrf_token != null && m_activexsrf.ContainsKey(xsrf_token)) + { + var expires = DateTime.UtcNow.AddMinutes(XSRF_TIMEOUT_MINUTES); + m_activexsrf[xsrf_token] = expires; + response.Cookies.Add(new ResponseCookie(XSRF_COOKIE_NAME, xsrf_token, expires)); + } + else + { + response.Status = System.Net.HttpStatusCode.BadRequest; + response.Reason = "Missing XSRF Token. Please reload the page"; + + return true; + } + } + + if (string.IsNullOrWhiteSpace(FIXMEGlobal.DataConnection.ApplicationSettings.WebserverPassword)) + return false; + + foreach(var k in (from n in m_activeTokens where DateTime.UtcNow > n.Value select n.Key)) + m_activeTokens.TryRemove(k, out _); + + + // If we have a valid token, proceed + if (!string.IsNullOrWhiteSpace(auth_token)) + { + DateTime expires; + var found = m_activeTokens.TryGetValue(auth_token, out expires); + if (!found) + { + auth_token = Duplicati.Library.Utility.Uri.UrlDecode(auth_token); + found = m_activeTokens.TryGetValue(auth_token, out expires); + } + + if (found && DateTime.UtcNow < expires) + { + expires = DateTime.UtcNow.AddHours(1); + + m_activeTokens[auth_token] = expires; + response.Cookies.Add(new ResponseCookie(AUTH_COOKIE_NAME, auth_token, expires)); + return false; + } + } + + if ("/".Equals(request.Uri.AbsolutePath, StringComparison.OrdinalIgnoreCase) || "/index.html".Equals(request.Uri.AbsolutePath, StringComparison.OrdinalIgnoreCase)) + { + response.Redirect("/login.html"); + return true; + } + + if (limitedAccess) + { + response.Status = System.Net.HttpStatusCode.Unauthorized; + response.Reason = "Not logged in"; + response.AddHeader("Location", "login.html"); + + return true; + } + + return false; + } + } +} + diff --git a/Duplicati/Server/WebServer/BodyWriter.cs b/Duplicati.Library.RestAPI/WebServer/BodyWriter.cs similarity index 96% rename from Duplicati/Server/WebServer/BodyWriter.cs rename to Duplicati.Library.RestAPI/WebServer/BodyWriter.cs index 9f332c32a..0434bfe8a 100644 --- a/Duplicati/Server/WebServer/BodyWriter.cs +++ b/Duplicati.Library.RestAPI/WebServer/BodyWriter.cs @@ -1,97 +1,97 @@ -// Copyright (C) 2015, The Duplicati Team - -// http://www.duplicati.com, info@duplicati.com -// -// This library is free software; you can redistribute it and/or modify -// it under the terms of the GNU Lesser General Public License as -// published by the Free Software Foundation; either version 2.1 of the -// License, or (at your option) any later version. -// -// This library is distributed in the hope that it will be useful, but -// WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU -// Lesser General Public License for more details. -// -// You should have received a copy of the GNU Lesser General Public -// License along with this library; if not, write to the Free Software -// Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA -using System; -using Duplicati.Server.Serialization; - -namespace Duplicati.Server.WebServer -{ - public class BodyWriter : System.IO.StreamWriter, IDisposable - { - private readonly HttpServer.IHttpResponse m_resp; - private readonly string m_jsonp; - private static readonly object SUCCESS_RESPONSE = new { Status = "OK" }; - - // We override the format provider so all JSON output uses US format - public override IFormatProvider FormatProvider - { - get { return System.Globalization.CultureInfo.InvariantCulture; } - } - - public BodyWriter(HttpServer.IHttpResponse resp, HttpServer.IHttpRequest request) - : this(resp, request.QueryString["jsonp"].Value) - { - } - - public BodyWriter(HttpServer.IHttpResponse resp, string jsonp) - : base(resp.Body, resp.Encoding) - { - m_resp = resp; - m_jsonp = jsonp; - if (!m_resp.HeadersSent) - m_resp.AddHeader("Cache-Control", "no-cache, no-store, must-revalidate, max-age=0"); - } - - protected override void Dispose (bool disposing) - { - if (!m_resp.HeadersSent) - { - base.Flush(); - m_resp.ContentLength = base.BaseStream.Length; - m_resp.Send(); - } - base.Dispose(disposing); - } - - public void SetOK() - { - m_resp.Reason = "OK"; - m_resp.Status = System.Net.HttpStatusCode.OK; - } - - public void OutputOK(object result = null) - { - SetOK(); - WriteJsonObject(result ?? SUCCESS_RESPONSE); - } - - public void WriteJsonObject(object o) - { - if (!m_resp.HeadersSent) - m_resp.ContentType = "application/json"; - - using(this) - { - if (!string.IsNullOrEmpty(m_jsonp)) - { - this.Write(m_jsonp); - this.Write('('); - } - - Serializer.SerializeJson(this, o, true); - - if (!string.IsNullOrEmpty(m_jsonp)) - { - this.Write(')'); - this.Flush(); - } - } - } - } - -} - +// Copyright (C) 2015, The Duplicati Team + +// http://www.duplicati.com, info@duplicati.com +// +// This library is free software; you can redistribute it and/or modify +// it under the terms of the GNU Lesser General Public License as +// published by the Free Software Foundation; either version 2.1 of the +// License, or (at your option) any later version. +// +// This library is distributed in the hope that it will be useful, but +// WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +// Lesser General Public License for more details. +// +// You should have received a copy of the GNU Lesser General Public +// License along with this library; if not, write to the Free Software +// Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA +using System; +using Duplicati.Server.Serialization; + +namespace Duplicati.Server.WebServer +{ + public class BodyWriter : System.IO.StreamWriter, IDisposable + { + private readonly HttpServer.IHttpResponse m_resp; + private readonly string m_jsonp; + private static readonly object SUCCESS_RESPONSE = new { Status = "OK" }; + + // We override the format provider so all JSON output uses US format + public override IFormatProvider FormatProvider + { + get { return System.Globalization.CultureInfo.InvariantCulture; } + } + + public BodyWriter(HttpServer.IHttpResponse resp, HttpServer.IHttpRequest request) + : this(resp, request.QueryString["jsonp"].Value) + { + } + + public BodyWriter(HttpServer.IHttpResponse resp, string jsonp) + : base(resp.Body, resp.Encoding) + { + m_resp = resp; + m_jsonp = jsonp; + if (!m_resp.HeadersSent) + m_resp.AddHeader("Cache-Control", "no-cache, no-store, must-revalidate, max-age=0"); + } + + protected override void Dispose (bool disposing) + { + if (!m_resp.HeadersSent) + { + base.Flush(); + m_resp.ContentLength = base.BaseStream.Length; + m_resp.Send(); + } + base.Dispose(disposing); + } + + public void SetOK() + { + m_resp.Reason = "OK"; + m_resp.Status = System.Net.HttpStatusCode.OK; + } + + public void OutputOK(object result = null) + { + SetOK(); + WriteJsonObject(result ?? SUCCESS_RESPONSE); + } + + public void WriteJsonObject(object o) + { + if (!m_resp.HeadersSent) + m_resp.ContentType = "application/json"; + + using(this) + { + if (!string.IsNullOrEmpty(m_jsonp)) + { + this.Write(m_jsonp); + this.Write('('); + } + + Serializer.SerializeJson(this, o, true); + + if (!string.IsNullOrEmpty(m_jsonp)) + { + this.Write(')'); + this.Flush(); + } + } + } + } + +} + diff --git a/Duplicati/Server/WebServer/CaptchaUtil.cs b/Duplicati.Library.RestAPI/WebServer/CaptchaUtil.cs similarity index 98% rename from Duplicati/Server/WebServer/CaptchaUtil.cs rename to Duplicati.Library.RestAPI/WebServer/CaptchaUtil.cs index c7ac7e471..632018de2 100644 --- a/Duplicati/Server/WebServer/CaptchaUtil.cs +++ b/Duplicati.Library.RestAPI/WebServer/CaptchaUtil.cs @@ -1,129 +1,129 @@ -// Copyright (C) 2016, The Duplicati Team -// http://www.duplicati.com, info@duplicati.com -// -// This library is free software; you can redistribute it and/or modify -// it under the terms of the GNU Lesser General Public License as -// published by the Free Software Foundation; either version 2.1 of the -// License, or (at your option) any later version. -// -// This library is distributed in the hope that it will be useful, but -// WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU -// Lesser General Public License for more details. -// -// You should have received a copy of the GNU Lesser General Public -// License along with this library; if not, write to the Free Software -// Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA -using System; -using System.Linq; -using System.Drawing; -using System.Drawing.Drawing2D; -using System.Drawing.Text; - -namespace Duplicati.Server.WebServer -{ - /// - /// Helper class for creating Captcha images - /// - public static class CaptchaUtil - { - /// - /// A lookup string with characters to use - /// - private static readonly string DEFAULT_CHARS = "ACDEFGHJKLMNPQRTUVWXY34679"; - - /// - /// A range of possible brush colors - /// - private static readonly Brush[] BRUSH_COLORS = - typeof(Brushes) - .GetProperties(System.Reflection.BindingFlags.Static | System.Reflection.BindingFlags.Public) - .Where(x => x.PropertyType == typeof(Brush)) - .Select(x => x.GetValue(null, null) as Brush) - .Where(x => x != null) - .ToArray(); - - /// - /// Approximate the size in pixels of text drawn at the given fontsize - /// - private static int ApproxTextWidth(string text, FontFamily fontfamily, int fontsize) - { - using (var font = new Font(fontfamily, fontsize, GraphicsUnit.Pixel)) - using (var graphics = Graphics.FromImage(new Bitmap(1, 1))) { - return (int) graphics.MeasureString(text, font).Width; - } - } - - /// - /// Creates a random answer. - /// - /// The random answer. - /// The list of allowed chars, supply a character multiple times to change frequency. - /// The minimum answer length. - /// The maximum answer length. - public static string CreateRandomAnswer(string allowedchars = null, int minlength = 10, int maxlength = 12) - { - allowedchars = allowedchars ?? DEFAULT_CHARS; - var rnd = new Random(); - var len = rnd.Next(Math.Min(minlength, maxlength), Math.Max(minlength, maxlength) + 1); - if (len <= 0) - throw new ArgumentException($"The values ${minlength} and ${maxlength} gave a final length of {len} and it must be greater than 0"); - - return new string(Enumerable.Range(0, len).Select(x => allowedchars[rnd.Next(0, allowedchars.Length)]).ToArray()); - } - - /// - /// Creates a captcha image. - /// - /// The captcha image. - /// The captcha solution string. - /// The size of the image, omit to get a size based on the string. - /// The size of the font used to create the captcha, in pixels. - public static Bitmap CreateCaptcha(string answer, Size size = default(Size), int fontsize = 40) - { - var fontfamily = FontFamily.GenericSansSerif; - var text_width = ApproxTextWidth(answer, fontfamily, fontsize); - if (size.Width == 0 || size.Height == 0) - size = new Size((int) (text_width * 1.2), (int) (fontsize * 1.2)); - - var bmp = new Bitmap(size.Width, size.Height); - var rnd = new Random(); - var stray_x = fontsize / 2; - var stray_y = size.Height / 4; - var ans_stray_x = fontsize / 3; - var ans_stray_y = size.Height / 6; - using (var graphics = Graphics.FromImage(bmp)) - using (var font1 = new Font(fontfamily, fontsize, GraphicsUnit.Pixel)) - using (var font2 = new Font(fontfamily, fontsize, GraphicsUnit.Pixel)) - using (var font3 = new HatchBrush(HatchStyle.Shingle, Color.GhostWhite, Color.DarkBlue)) - { - graphics.Clear(Color.White); - graphics.TextRenderingHint = TextRenderingHint.AntiAlias; - - // Apply a some background string to make it hard to do OCR - foreach (var color in new[] { Color.Yellow, Color.LightGreen, Color.GreenYellow }) - using (var brush = new SolidBrush(color)) - graphics.DrawString(CreateRandomAnswer(minlength: answer.Length, maxlength: answer.Length), font2, brush, rnd.Next(-stray_x, stray_x), rnd.Next(-stray_y, stray_y)); - - - var spacing = (size.Width / fontsize) + rnd.Next(0, stray_x); - - // Create a vertical background lines - for (var i = rnd.Next(0, stray_x); i < size.Width; i += spacing) - using (var pen = new Pen(BRUSH_COLORS[rnd.Next(0, BRUSH_COLORS.Length)])) - graphics.DrawLine(pen, i + rnd.Next(-stray_x, stray_x), rnd.Next(0, stray_y), i + rnd.Next(-stray_x, stray_x), size.Height - rnd.Next(0, stray_y)); - - spacing = (size.Height / fontsize) + rnd.Next(0, stray_y); - // Create a horizontal background lines - for (var i = rnd.Next(0, stray_y); i < size.Height; i += spacing) - using (var pen = new Pen(BRUSH_COLORS[rnd.Next(0, BRUSH_COLORS.Length)])) - graphics.DrawLine(pen, rnd.Next(0, stray_x), i + rnd.Next(-stray_y, stray_y), size.Width - rnd.Next(0, stray_x), i + rnd.Next(-stray_y, stray_y)); - - // Draw the actual answer - graphics.DrawString(answer, font1, font3, ((size.Width - text_width) / 2) + rnd.Next(-ans_stray_x, ans_stray_x), ((size.Height - fontsize) / 2) + rnd.Next(-ans_stray_y, ans_stray_y)); - - return bmp; - } - } - } -} +// Copyright (C) 2016, The Duplicati Team +// http://www.duplicati.com, info@duplicati.com +// +// This library is free software; you can redistribute it and/or modify +// it under the terms of the GNU Lesser General Public License as +// published by the Free Software Foundation; either version 2.1 of the +// License, or (at your option) any later version. +// +// This library is distributed in the hope that it will be useful, but +// WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +// Lesser General Public License for more details. +// +// You should have received a copy of the GNU Lesser General Public +// License along with this library; if not, write to the Free Software +// Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA +using System; +using System.Linq; +using System.Drawing; +using System.Drawing.Drawing2D; +using System.Drawing.Text; + +namespace Duplicati.Server.WebServer +{ + /// + /// Helper class for creating Captcha images + /// + public static class CaptchaUtil + { + /// + /// A lookup string with characters to use + /// + private static readonly string DEFAULT_CHARS = "ACDEFGHJKLMNPQRTUVWXY34679"; + + /// + /// A range of possible brush colors + /// + private static readonly Brush[] BRUSH_COLORS = + typeof(Brushes) + .GetProperties(System.Reflection.BindingFlags.Static | System.Reflection.BindingFlags.Public) + .Where(x => x.PropertyType == typeof(Brush)) + .Select(x => x.GetValue(null, null) as Brush) + .Where(x => x != null) + .ToArray(); + + /// + /// Approximate the size in pixels of text drawn at the given fontsize + /// + private static int ApproxTextWidth(string text, FontFamily fontfamily, int fontsize) + { + using (var font = new Font(fontfamily, fontsize, GraphicsUnit.Pixel)) + using (var graphics = Graphics.FromImage(new Bitmap(1, 1))) { + return (int) graphics.MeasureString(text, font).Width; + } + } + + /// + /// Creates a random answer. + /// + /// The random answer. + /// The list of allowed chars, supply a character multiple times to change frequency. + /// The minimum answer length. + /// The maximum answer length. + public static string CreateRandomAnswer(string allowedchars = null, int minlength = 10, int maxlength = 12) + { + allowedchars = allowedchars ?? DEFAULT_CHARS; + var rnd = new Random(); + var len = rnd.Next(Math.Min(minlength, maxlength), Math.Max(minlength, maxlength) + 1); + if (len <= 0) + throw new ArgumentException($"The values ${minlength} and ${maxlength} gave a final length of {len} and it must be greater than 0"); + + return new string(Enumerable.Range(0, len).Select(x => allowedchars[rnd.Next(0, allowedchars.Length)]).ToArray()); + } + + /// + /// Creates a captcha image. + /// + /// The captcha image. + /// The captcha solution string. + /// The size of the image, omit to get a size based on the string. + /// The size of the font used to create the captcha, in pixels. + public static Bitmap CreateCaptcha(string answer, Size size = default(Size), int fontsize = 40) + { + var fontfamily = FontFamily.GenericSansSerif; + var text_width = ApproxTextWidth(answer, fontfamily, fontsize); + if (size.Width == 0 || size.Height == 0) + size = new Size((int) (text_width * 1.2), (int) (fontsize * 1.2)); + + var bmp = new Bitmap(size.Width, size.Height); + var rnd = new Random(); + var stray_x = fontsize / 2; + var stray_y = size.Height / 4; + var ans_stray_x = fontsize / 3; + var ans_stray_y = size.Height / 6; + using (var graphics = Graphics.FromImage(bmp)) + using (var font1 = new Font(fontfamily, fontsize, GraphicsUnit.Pixel)) + using (var font2 = new Font(fontfamily, fontsize, GraphicsUnit.Pixel)) + using (var font3 = new HatchBrush(HatchStyle.Shingle, Color.GhostWhite, Color.DarkBlue)) + { + graphics.Clear(Color.White); + graphics.TextRenderingHint = TextRenderingHint.AntiAlias; + + // Apply a some background string to make it hard to do OCR + foreach (var color in new[] { Color.Yellow, Color.LightGreen, Color.GreenYellow }) + using (var brush = new SolidBrush(color)) + graphics.DrawString(CreateRandomAnswer(minlength: answer.Length, maxlength: answer.Length), font2, brush, rnd.Next(-stray_x, stray_x), rnd.Next(-stray_y, stray_y)); + + + var spacing = (size.Width / fontsize) + rnd.Next(0, stray_x); + + // Create a vertical background lines + for (var i = rnd.Next(0, stray_x); i < size.Width; i += spacing) + using (var pen = new Pen(BRUSH_COLORS[rnd.Next(0, BRUSH_COLORS.Length)])) + graphics.DrawLine(pen, i + rnd.Next(-stray_x, stray_x), rnd.Next(0, stray_y), i + rnd.Next(-stray_x, stray_x), size.Height - rnd.Next(0, stray_y)); + + spacing = (size.Height / fontsize) + rnd.Next(0, stray_y); + // Create a horizontal background lines + for (var i = rnd.Next(0, stray_y); i < size.Height; i += spacing) + using (var pen = new Pen(BRUSH_COLORS[rnd.Next(0, BRUSH_COLORS.Length)])) + graphics.DrawLine(pen, rnd.Next(0, stray_x), i + rnd.Next(-stray_y, stray_y), size.Width - rnd.Next(0, stray_x), i + rnd.Next(-stray_y, stray_y)); + + // Draw the actual answer + graphics.DrawString(answer, font1, font3, ((size.Width - text_width) / 2) + rnd.Next(-ans_stray_x, ans_stray_x), ((size.Height - fontsize) / 2) + rnd.Next(-ans_stray_y, ans_stray_y)); + + return bmp; + } + } + } +} diff --git a/Duplicati/Server/WebServer/IndexHtmlHandler.cs b/Duplicati.Library.RestAPI/WebServer/IndexHtmlHandler.cs similarity index 97% rename from Duplicati/Server/WebServer/IndexHtmlHandler.cs rename to Duplicati.Library.RestAPI/WebServer/IndexHtmlHandler.cs index 508264719..3032aa355 100644 --- a/Duplicati/Server/WebServer/IndexHtmlHandler.cs +++ b/Duplicati.Library.RestAPI/WebServer/IndexHtmlHandler.cs @@ -1,79 +1,79 @@ -// Copyright (C) 2015, The Duplicati Team - -// http://www.duplicati.com, info@duplicati.com -// -// This library is free software; you can redistribute it and/or modify -// it under the terms of the GNU Lesser General Public License as -// published by the Free Software Foundation; either version 2.1 of the -// License, or (at your option) any later version. -// -// This library is distributed in the hope that it will be useful, but -// WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU -// Lesser General Public License for more details. -// -// You should have received a copy of the GNU Lesser General Public -// License along with this library; if not, write to the Free Software -// Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA -using System; -using System.Linq; -using HttpServer; -using HttpServer.HttpModules; -using HttpServer.Exceptions; +// Copyright (C) 2015, The Duplicati Team + +// http://www.duplicati.com, info@duplicati.com +// +// This library is free software; you can redistribute it and/or modify +// it under the terms of the GNU Lesser General Public License as +// published by the Free Software Foundation; either version 2.1 of the +// License, or (at your option) any later version. +// +// This library is distributed in the hope that it will be useful, but +// WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +// Lesser General Public License for more details. +// +// You should have received a copy of the GNU Lesser General Public +// License along with this library; if not, write to the Free Software +// Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA +using System; +using System.Linq; +using HttpServer; +using HttpServer.HttpModules; +using HttpServer.Exceptions; using Duplicati.Library.Common.IO; -namespace Duplicati.Server.WebServer -{ - internal class IndexHtmlHandler : HttpModule - { - private readonly string m_webroot; - - private static readonly string[] ForbiddenChars = new string[] {"\\", "..", ":"}.Union(from n in System.IO.Path.GetInvalidPathChars() select n.ToString()).Distinct().ToArray(); - private static readonly string DirSep = Util.DirectorySeparatorString; - - public IndexHtmlHandler(string webroot) { m_webroot = webroot; } - - public override bool Process(HttpServer.IHttpRequest request, HttpServer.IHttpResponse response, HttpServer.Sessions.IHttpSession session) - { - var path = this.GetPath(request.Uri); - var html = System.IO.Path.Combine(path, "index.html"); - var htm = System.IO.Path.Combine(path, "index.htm"); - - if (System.IO.Directory.Exists(path) && (System.IO.File.Exists(html) || System.IO.File.Exists(htm))) - { - if (!request.Uri.AbsolutePath.EndsWith("/", StringComparison.Ordinal)) - { - response.Redirect(request.Uri.AbsolutePath + "/"); - return true; - } - - response.Status = System.Net.HttpStatusCode.OK; - response.Reason = "OK"; - response.ContentType = "text/html; charset=utf-8"; - response.AddHeader("Cache-Control", "no-cache, no-store, must-revalidate, max-age=0"); - - using (var fs = System.IO.File.OpenRead(System.IO.File.Exists(html) ? html : htm)) - { - response.ContentLength = fs.Length; - response.Body = fs; - response.Send(); - } - - return true; - } - - return false; - } - - private string GetPath(Uri uri) - { - if (ForbiddenChars.Any(x => uri.AbsolutePath.Contains(x))) - throw new BadRequestException("Illegal path"); - var uripath = Uri.UnescapeDataString(uri.AbsolutePath); - while(uripath.Length > 0 && (uripath.StartsWith("/", StringComparison.Ordinal) || uripath.StartsWith(DirSep, StringComparison.Ordinal))) - uripath = uripath.Substring(1); - return System.IO.Path.Combine(m_webroot, uripath.Replace('/', System.IO.Path.DirectorySeparatorChar)); - } - } -} - +namespace Duplicati.Server.WebServer +{ + internal class IndexHtmlHandler : HttpModule + { + private readonly string m_webroot; + + private static readonly string[] ForbiddenChars = new string[] {"\\", "..", ":"}.Union(from n in System.IO.Path.GetInvalidPathChars() select n.ToString()).Distinct().ToArray(); + private static readonly string DirSep = Util.DirectorySeparatorString; + + public IndexHtmlHandler(string webroot) { m_webroot = webroot; } + + public override bool Process(HttpServer.IHttpRequest request, HttpServer.IHttpResponse response, HttpServer.Sessions.IHttpSession session) + { + var path = this.GetPath(request.Uri); + var html = System.IO.Path.Combine(path, "index.html"); + var htm = System.IO.Path.Combine(path, "index.htm"); + + if (System.IO.Directory.Exists(path) && (System.IO.File.Exists(html) || System.IO.File.Exists(htm))) + { + if (!request.Uri.AbsolutePath.EndsWith("/", StringComparison.Ordinal)) + { + response.Redirect(request.Uri.AbsolutePath + "/"); + return true; + } + + response.Status = System.Net.HttpStatusCode.OK; + response.Reason = "OK"; + response.ContentType = "text/html; charset=utf-8"; + response.AddHeader("Cache-Control", "no-cache, no-store, must-revalidate, max-age=0"); + + using (var fs = System.IO.File.OpenRead(System.IO.File.Exists(html) ? html : htm)) + { + response.ContentLength = fs.Length; + response.Body = fs; + response.Send(); + } + + return true; + } + + return false; + } + + private string GetPath(Uri uri) + { + if (ForbiddenChars.Any(x => uri.AbsolutePath.Contains(x))) + throw new BadRequestException("Illegal path"); + var uripath = Uri.UnescapeDataString(uri.AbsolutePath); + while(uripath.Length > 0 && (uripath.StartsWith("/", StringComparison.Ordinal) || uripath.StartsWith(DirSep, StringComparison.Ordinal))) + uripath = uripath.Substring(1); + return System.IO.Path.Combine(m_webroot, uripath.Replace('/', System.IO.Path.DirectorySeparatorChar)); + } + } +} + diff --git a/Duplicati/Server/WebServer/RESTHandler.cs b/Duplicati.Library.RestAPI/WebServer/RESTHandler.cs similarity index 95% rename from Duplicati/Server/WebServer/RESTHandler.cs rename to Duplicati.Library.RestAPI/WebServer/RESTHandler.cs index b09b78378..ea931607d 100644 --- a/Duplicati/Server/WebServer/RESTHandler.cs +++ b/Duplicati.Library.RestAPI/WebServer/RESTHandler.cs @@ -1,258 +1,259 @@ -// Copyright (C) 2015, The Duplicati Team -// http://www.duplicati.com, info@duplicati.com -// -// This library is free software; you can redistribute it and/or modify -// it under the terms of the GNU Lesser General Public License as -// published by the Free Software Foundation; either version 2.1 of the -// License, or (at your option) any later version. -// -// This library is distributed in the hope that it will be useful, but -// WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU -// Lesser General Public License for more details. -// -// You should have received a copy of the GNU Lesser General Public -// License along with this library; if not, write to the Free Software -// Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA -using System; -using System.Collections.Concurrent; -using System.Collections.Generic; -using System.Linq; -using HttpServer.HttpModules; - -using Duplicati.Server.WebServer.RESTMethods; - -namespace Duplicati.Server.WebServer -{ - public class RESTHandler : HttpModule - { - public const string API_URI_PATH = "/api/v1"; - public static readonly int API_URI_SEGMENTS = API_URI_PATH.Split(new char[] {'/'}).Length; - - private static readonly Dictionary _modules = new Dictionary(StringComparer.OrdinalIgnoreCase); - - public static IDictionary Modules { get { return _modules; } } - - /// - /// Loads all REST modules in the Duplicati.Server.WebServer.RESTMethods namespace - /// - static RESTHandler() - { - var lst = - from n in typeof(RESTHandler).Assembly.GetTypes() - where - n.Namespace == typeof(IRESTMethod).Namespace - && - typeof(IRESTMethod).IsAssignableFrom(n) - && - !n.IsAbstract - && - !n.IsInterface - select n; - - foreach(var t in lst) - { - var m = (IRESTMethod)Activator.CreateInstance(t); - _modules.Add(t.Name.ToLowerInvariant(), m); - } - } - - public static void HandleControlCGI(HttpServer.IHttpRequest request, HttpServer.IHttpResponse response, HttpServer.Sessions.IHttpSession session, Type module) - { - var method = request.Method; - if (!string.IsNullOrWhiteSpace(request.Headers["X-HTTP-Method-Override"])) - method = request.Headers["X-HTTP-Method-Override"]; - - DoProcess(request, response, session, method, module.Name.ToLowerInvariant(), (String.Equals(request.Method, "POST", StringComparison.OrdinalIgnoreCase) ? request.Form : request.QueryString)["id"].Value); - } - - private static readonly ConcurrentDictionary _cultureCache = new ConcurrentDictionary(StringComparer.OrdinalIgnoreCase); - - private static System.Globalization.CultureInfo ParseRequestCulture(RequestInfo info) - { - // Inject the override - return ParseRequestCulture(string.Format("{0},{1}", info.Request.Headers["X-UI-Language"], info.Request.Headers["Accept-Language"])); - } - - public static System.Globalization.CultureInfo ParseDefaultRequestCulture(RequestInfo info) - { - if (info == null) - return null; - return ParseRequestCulture(info.Request.Headers["Accept-Language"]); - } - - private static System.Globalization.CultureInfo ParseRequestCulture(string acceptheader) - { - acceptheader = acceptheader ?? string.Empty; - - // Lock-free read - System.Globalization.CultureInfo ci; - if (_cultureCache.TryGetValue(acceptheader, out ci)) - return ci; - - // Lock-free assignment, we might compute the value twice - return _cultureCache[acceptheader] = - // Parse headers like "Accept-Language: da, en-gb;q=0.8, en;q=0.7" - acceptheader - .Split(new[] { "," }, StringSplitOptions.RemoveEmptyEntries) - .Select(x => - { - var opts = x.Split(new[] { ";" }, StringSplitOptions.RemoveEmptyEntries); - var lang = opts.FirstOrDefault(); - var weight = - opts.Where(y => y.StartsWith("q=", StringComparison.OrdinalIgnoreCase)) - .Select(y => - { - float f; - float.TryParse(y.Substring(2), System.Globalization.NumberStyles.Float, System.Globalization.CultureInfo.InvariantCulture, out f); - return f; - }).FirstOrDefault(); - - // Set the default weight=1 - if (weight <= 0.001 && weight >= 0) - weight = 1; - - return new KeyValuePair(lang, weight); - }) - // Handle priority - .OrderByDescending(x => x.Value) - .Select(x => x.Key) - .Distinct() - // Filter invalid/unsupported items - .Where(x => !string.IsNullOrWhiteSpace(x) && Library.Localization.LocalizationService.ParseCulture(x) != null) - .Select(x => Library.Localization.LocalizationService.ParseCulture(x)) - // And get the first that works - .FirstOrDefault(); - - } - - public static void DoProcess(RequestInfo info, string method, string module, string key) - { - var ci = ParseRequestCulture(info); - - using (Library.Localization.LocalizationService.TemporaryContext(ci)) - { - try - { - if (ci != null) - info.Response.AddHeader("Content-Language", ci.Name); - - IRESTMethod mod; - _modules.TryGetValue(module, out mod); - - if (mod == null) - { - info.Response.Status = System.Net.HttpStatusCode.NotFound; - info.Response.Reason = "No such module"; - } - else if (method == HttpServer.Method.Get && mod is IRESTMethodGET get) - { - if (info.Request.Form != HttpServer.HttpForm.EmptyForm) - { - if (info.Request.QueryString == HttpServer.HttpInput.Empty) - { - var r = info.Request.GetType().GetField("_queryString", System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Instance); - r.SetValue(info.Request, new HttpServer.HttpInput("formdata")); - } - - foreach (HttpServer.HttpInputItem v in info.Request.Form) - if (!info.Request.QueryString.Contains(v.Name)) - info.Request.QueryString.Add(v.Name, v.Value); - } - - get.GET(key, info); - } - else if (method == HttpServer.Method.Put && mod is IRESTMethodPUT put) - put.PUT(key, info); - else if (method == HttpServer.Method.Post && mod is IRESTMethodPOST post) - { - if (info.Request.Form == HttpServer.HttpForm.EmptyForm || info.Request.Form == HttpServer.HttpInput.Empty) - { - var r = info.Request.GetType().GetMethod("AssignForm", System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Instance, null, new Type[] {typeof(HttpServer.HttpForm)}, null); - r.Invoke(info.Request, new object[] {new HttpServer.HttpForm(info.Request.QueryString)}); - } - else - { - foreach (HttpServer.HttpInputItem v in info.Request.QueryString) - if (!info.Request.Form.Contains(v.Name)) - info.Request.Form.Add(v.Name, v.Value); - } - - post.POST(key, info); - } - else if (method == HttpServer.Method.Delete && mod is IRESTMethodDELETE delete) - delete.DELETE(key, info); - else if (method == "PATCH" && mod is IRESTMethodPATCH patch) - patch.PATCH(key, info); - else - { - info.Response.Status = System.Net.HttpStatusCode.MethodNotAllowed; - info.Response.Reason = "Method is not allowed"; - } - } - catch (Exception ex) - { - Program.DataConnection.LogError("", string.Format("Request for {0} gave error", info.Request.Uri), ex); - Console.WriteLine(ex); - - try - { - if (!info.Response.HeadersSent) - { - info.Response.Status = System.Net.HttpStatusCode.InternalServerError; - info.Response.Reason = "Error"; - info.Response.ContentType = "text/plain"; - - var wex = ex; - while (wex is System.Reflection.TargetInvocationException && wex.InnerException != wex) - wex = wex.InnerException; - - info.BodyWriter.WriteJsonObject(new - { - Message = wex.Message, - Type = wex.GetType().Name, -#if DEBUG - Stacktrace = wex.ToString() -#endif - }); - info.BodyWriter.Flush(); - } - } - catch (Exception flex) - { - Program.DataConnection.LogError("", "Reporting error gave error", flex); - } - } - } - } - - public static void DoProcess(HttpServer.IHttpRequest request, HttpServer.IHttpResponse response, HttpServer.Sessions.IHttpSession session, string method, string module, string key) - { - using(var reqinfo = new RequestInfo(request, response, session)) - DoProcess(reqinfo, method, module, key); - } - - public override bool Process(HttpServer.IHttpRequest request, HttpServer.IHttpResponse response, HttpServer.Sessions.IHttpSession session) - { - if (!request.Uri.AbsolutePath.StartsWith(API_URI_PATH, StringComparison.OrdinalIgnoreCase)) - return false; - - var module = request.Uri.Segments.Skip(API_URI_SEGMENTS).FirstOrDefault(); - if (string.IsNullOrWhiteSpace(module)) - module = "help"; - - module = module.Trim('/'); - - var key = string.Join("", request.Uri.Segments.Skip(API_URI_SEGMENTS + 1)).Trim('/'); - - var method = request.Method; - if (!string.IsNullOrWhiteSpace(request.Headers["X-HTTP-Method-Override"])) - method = request.Headers["X-HTTP-Method-Override"]; - - DoProcess(request, response, session, method, module, key); - - return true; - } - } -} - +// Copyright (C) 2015, The Duplicati Team +// http://www.duplicati.com, info@duplicati.com +// +// This library is free software; you can redistribute it and/or modify +// it under the terms of the GNU Lesser General Public License as +// published by the Free Software Foundation; either version 2.1 of the +// License, or (at your option) any later version. +// +// This library is distributed in the hope that it will be useful, but +// WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +// Lesser General Public License for more details. +// +// You should have received a copy of the GNU Lesser General Public +// License along with this library; if not, write to the Free Software +// Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA +using System; +using System.Collections.Concurrent; +using System.Collections.Generic; +using System.Linq; +using HttpServer.HttpModules; + +using Duplicati.Server.WebServer.RESTMethods; +using Duplicati.Library.RestAPI; + +namespace Duplicati.Server.WebServer +{ + public class RESTHandler : HttpModule + { + public const string API_URI_PATH = "/api/v1"; + public static readonly int API_URI_SEGMENTS = API_URI_PATH.Split(new char[] {'/'}).Length; + + private static readonly Dictionary _modules = new Dictionary(StringComparer.OrdinalIgnoreCase); + + public static IDictionary Modules { get { return _modules; } } + + /// + /// Loads all REST modules in the Duplicati.Server.WebServer.RESTMethods namespace + /// + static RESTHandler() + { + var lst = + from n in typeof(IRESTMethod).Assembly.GetTypes() + where + n.Namespace == typeof(IRESTMethod).Namespace + && + typeof(IRESTMethod).IsAssignableFrom(n) + && + !n.IsAbstract + && + !n.IsInterface + select n; + + foreach(var t in lst) + { + var m = (IRESTMethod)Activator.CreateInstance(t); + _modules.Add(t.Name.ToLowerInvariant(), m); + } + } + + public static void HandleControlCGI(HttpServer.IHttpRequest request, HttpServer.IHttpResponse response, HttpServer.Sessions.IHttpSession session, Type module) + { + var method = request.Method; + if (!string.IsNullOrWhiteSpace(request.Headers["X-HTTP-Method-Override"])) + method = request.Headers["X-HTTP-Method-Override"]; + + DoProcess(request, response, session, method, module.Name.ToLowerInvariant(), (String.Equals(request.Method, "POST", StringComparison.OrdinalIgnoreCase) ? request.Form : request.QueryString)["id"].Value); + } + + private static readonly ConcurrentDictionary _cultureCache = new ConcurrentDictionary(StringComparer.OrdinalIgnoreCase); + + private static System.Globalization.CultureInfo ParseRequestCulture(RequestInfo info) + { + // Inject the override + return ParseRequestCulture(string.Format("{0},{1}", info.Request.Headers["X-UI-Language"], info.Request.Headers["Accept-Language"])); + } + + public static System.Globalization.CultureInfo ParseDefaultRequestCulture(RequestInfo info) + { + if (info == null) + return null; + return ParseRequestCulture(info.Request.Headers["Accept-Language"]); + } + + private static System.Globalization.CultureInfo ParseRequestCulture(string acceptheader) + { + acceptheader = acceptheader ?? string.Empty; + + // Lock-free read + System.Globalization.CultureInfo ci; + if (_cultureCache.TryGetValue(acceptheader, out ci)) + return ci; + + // Lock-free assignment, we might compute the value twice + return _cultureCache[acceptheader] = + // Parse headers like "Accept-Language: da, en-gb;q=0.8, en;q=0.7" + acceptheader + .Split(new[] { "," }, StringSplitOptions.RemoveEmptyEntries) + .Select(x => + { + var opts = x.Split(new[] { ";" }, StringSplitOptions.RemoveEmptyEntries); + var lang = opts.FirstOrDefault(); + var weight = + opts.Where(y => y.StartsWith("q=", StringComparison.OrdinalIgnoreCase)) + .Select(y => + { + float f; + float.TryParse(y.Substring(2), System.Globalization.NumberStyles.Float, System.Globalization.CultureInfo.InvariantCulture, out f); + return f; + }).FirstOrDefault(); + + // Set the default weight=1 + if (weight <= 0.001 && weight >= 0) + weight = 1; + + return new KeyValuePair(lang, weight); + }) + // Handle priority + .OrderByDescending(x => x.Value) + .Select(x => x.Key) + .Distinct() + // Filter invalid/unsupported items + .Where(x => !string.IsNullOrWhiteSpace(x) && Library.Localization.LocalizationService.ParseCulture(x) != null) + .Select(x => Library.Localization.LocalizationService.ParseCulture(x)) + // And get the first that works + .FirstOrDefault(); + + } + + public static void DoProcess(RequestInfo info, string method, string module, string key) + { + var ci = ParseRequestCulture(info); + + using (Library.Localization.LocalizationService.TemporaryContext(ci)) + { + try + { + if (ci != null) + info.Response.AddHeader("Content-Language", ci.Name); + + IRESTMethod mod; + _modules.TryGetValue(module, out mod); + + if (mod == null) + { + info.Response.Status = System.Net.HttpStatusCode.NotFound; + info.Response.Reason = "No such module"; + } + else if (method == HttpServer.Method.Get && mod is IRESTMethodGET get) + { + if (info.Request.Form != HttpServer.HttpForm.EmptyForm) + { + if (info.Request.QueryString == HttpServer.HttpInput.Empty) + { + var r = info.Request.GetType().GetField("_queryString", System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Instance); + r.SetValue(info.Request, new HttpServer.HttpInput("formdata")); + } + + foreach (HttpServer.HttpInputItem v in info.Request.Form) + if (!info.Request.QueryString.Contains(v.Name)) + info.Request.QueryString.Add(v.Name, v.Value); + } + + get.GET(key, info); + } + else if (method == HttpServer.Method.Put && mod is IRESTMethodPUT put) + put.PUT(key, info); + else if (method == HttpServer.Method.Post && mod is IRESTMethodPOST post) + { + if (info.Request.Form == HttpServer.HttpForm.EmptyForm || info.Request.Form == HttpServer.HttpInput.Empty) + { + var r = info.Request.GetType().GetMethod("AssignForm", System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Instance, null, new Type[] {typeof(HttpServer.HttpForm)}, null); + r.Invoke(info.Request, new object[] {new HttpServer.HttpForm(info.Request.QueryString)}); + } + else + { + foreach (HttpServer.HttpInputItem v in info.Request.QueryString) + if (!info.Request.Form.Contains(v.Name)) + info.Request.Form.Add(v.Name, v.Value); + } + + post.POST(key, info); + } + else if (method == HttpServer.Method.Delete && mod is IRESTMethodDELETE delete) + delete.DELETE(key, info); + else if (method == "PATCH" && mod is IRESTMethodPATCH patch) + patch.PATCH(key, info); + else + { + info.Response.Status = System.Net.HttpStatusCode.MethodNotAllowed; + info.Response.Reason = "Method is not allowed"; + } + } + catch (Exception ex) + { + FIXMEGlobal.DataConnection.LogError("", string.Format("Request for {0} gave error", info.Request.Uri), ex); + Console.WriteLine(ex); + + try + { + if (!info.Response.HeadersSent) + { + info.Response.Status = System.Net.HttpStatusCode.InternalServerError; + info.Response.Reason = "Error"; + info.Response.ContentType = "text/plain"; + + var wex = ex; + while (wex is System.Reflection.TargetInvocationException && wex.InnerException != wex) + wex = wex.InnerException; + + info.BodyWriter.WriteJsonObject(new + { + Message = wex.Message, + Type = wex.GetType().Name, +#if DEBUG + Stacktrace = wex.ToString() +#endif + }); + info.BodyWriter.Flush(); + } + } + catch (Exception flex) + { + FIXMEGlobal.DataConnection.LogError("", "Reporting error gave error", flex); + } + } + } + } + + public static void DoProcess(HttpServer.IHttpRequest request, HttpServer.IHttpResponse response, HttpServer.Sessions.IHttpSession session, string method, string module, string key) + { + using(var reqinfo = new RequestInfo(request, response, session)) + DoProcess(reqinfo, method, module, key); + } + + public override bool Process(HttpServer.IHttpRequest request, HttpServer.IHttpResponse response, HttpServer.Sessions.IHttpSession session) + { + if (!request.Uri.AbsolutePath.StartsWith(API_URI_PATH, StringComparison.OrdinalIgnoreCase)) + return false; + + var module = request.Uri.Segments.Skip(API_URI_SEGMENTS).FirstOrDefault(); + if (string.IsNullOrWhiteSpace(module)) + module = "help"; + + module = module.Trim('/'); + + var key = string.Join("", request.Uri.Segments.Skip(API_URI_SEGMENTS + 1)).Trim('/'); + + var method = request.Method; + if (!string.IsNullOrWhiteSpace(request.Headers["X-HTTP-Method-Override"])) + method = request.Headers["X-HTTP-Method-Override"]; + + DoProcess(request, response, session, method, module, key); + + return true; + } + } +} + diff --git a/Duplicati/Server/WebServer/Server.cs b/Duplicati.Library.RestAPI/WebServer/Server.cs similarity index 93% rename from Duplicati/Server/WebServer/Server.cs rename to Duplicati.Library.RestAPI/WebServer/Server.cs index e9d5396e5..6ea669629 100644 --- a/Duplicati/Server/WebServer/Server.cs +++ b/Duplicati.Library.RestAPI/WebServer/Server.cs @@ -1,454 +1,455 @@ -using System; -using System.Collections; -using System.Collections.Generic; -using System.Linq; -using HttpServer.HttpModules; -using System.Security.Cryptography.X509Certificates; -using Duplicati.Library.Common.IO; - -namespace Duplicati.Server.WebServer -{ - public class Server - { - /// - /// The tag used for logging - /// - private static readonly string LOGTAG = Duplicati.Library.Logging.Log.LogTagFromType(); - - /// - /// Option for changing the webroot folder - /// - public const string OPTION_WEBROOT = "webservice-webroot"; - - /// - /// Option for changing the webservice listen port - /// - public const string OPTION_PORT = "webservice-port"; - - /// - /// Option for changing the webservice listen interface - /// - public const string OPTION_INTERFACE = "webservice-interface"; - - /// - /// The default path to the web root - /// - public const string DEFAULT_OPTION_WEBROOT = "webroot"; - - /// - /// The default listening port - /// - public const int DEFAULT_OPTION_PORT = 8200; - - /// - /// Option for setting the webservice SSL certificate - /// - public const string OPTION_SSLCERTIFICATEFILE = "webservice-sslcertificatefile"; - - /// - /// Option for setting the webservice SSL certificate key - /// - public const string OPTION_SSLCERTIFICATEFILEPASSWORD = "webservice-sslcertificatepassword"; - - /// - /// The default listening interface - /// - public const string DEFAULT_OPTION_INTERFACE = "loopback"; - - /// - /// The single webserver instance - /// - private readonly HttpServer.HttpServer m_server; - - /// - /// The webserver listening port - /// - public readonly int Port; - - /// - /// A string that is sent out instead of password values - /// - public const string PASSWORD_PLACEHOLDER = "**********"; - - /// - /// Sets up the webserver and starts it - /// - /// A set of options - public Server(IDictionary options) - { - string portstring; - IEnumerable ports = null; - options.TryGetValue(OPTION_PORT, out portstring); - if (!string.IsNullOrEmpty(portstring)) - ports = - from n in portstring.Split(new char[] { ',' }, StringSplitOptions.RemoveEmptyEntries) - where int.TryParse(n, out _) - select int.Parse(n); - - if (ports == null || !ports.Any()) - ports = new int[] { DEFAULT_OPTION_PORT }; - - string interfacestring; - System.Net.IPAddress listenInterface; - options.TryGetValue(OPTION_INTERFACE, out interfacestring); - - if (string.IsNullOrWhiteSpace(interfacestring)) - interfacestring = Program.DataConnection.ApplicationSettings.ServerListenInterface; - if (string.IsNullOrWhiteSpace(interfacestring)) - interfacestring = DEFAULT_OPTION_INTERFACE; - - if (interfacestring.Trim() == "*" || interfacestring.Trim().Equals("any", StringComparison.OrdinalIgnoreCase) || interfacestring.Trim().Equals("all", StringComparison.OrdinalIgnoreCase)) - listenInterface = System.Net.IPAddress.Any; - else if (interfacestring.Trim() == "loopback") - listenInterface = System.Net.IPAddress.Loopback; - else - listenInterface = System.Net.IPAddress.Parse(interfacestring); - - string certificateFile; - options.TryGetValue(OPTION_SSLCERTIFICATEFILE, out certificateFile); - - string certificateFilePassword; - options.TryGetValue(OPTION_SSLCERTIFICATEFILEPASSWORD, out certificateFilePassword); - - X509Certificate2 cert = null; - bool certValid = false; - - if (certificateFile == null) - { - try - { - cert = Program.DataConnection.ApplicationSettings.ServerSSLCertificate; - - if (cert != null) - certValid = cert.HasPrivateKey; - } - catch (Exception ex) - { - Duplicati.Library.Logging.Log.WriteWarningMessage(LOGTAG, "DefectStoredSSLCert", ex, Strings.Server.DefectSSLCertInDatabase); - } - } - else if (certificateFile.Length == 0) - { - Program.DataConnection.ApplicationSettings.ServerSSLCertificate = null; - } - else - { - try - { - if (string.IsNullOrWhiteSpace(certificateFilePassword)) - cert = new X509Certificate2(certificateFile, "", X509KeyStorageFlags.Exportable); - else - cert = new X509Certificate2(certificateFile, certificateFilePassword, X509KeyStorageFlags.Exportable); - - certValid = cert.HasPrivateKey; - } - catch (Exception ex) - { - throw new Exception(Strings.Server.SSLCertificateFailure(ex.Message), ex); - } - } - - // If we are in hosted mode with no specified port, - // then try different ports - foreach (var p in ports) - try - { - // Due to the way the server is initialized, - // we cannot try to start it again on another port, - // so we create a new server for each attempt - - var server = CreateServer(options); - - if (!certValid) - server.Start(listenInterface, p); - else - { - var secProtocols = System.Security.Authentication.SslProtocols.Tls12; - - try - { - //try TLS 1.3 (type not available on .NET < 4.8) - secProtocols = System.Security.Authentication.SslProtocols.Tls12 | (System.Security.Authentication.SslProtocols)12288; - } - catch (NotSupportedException) - { - } - server.Start(listenInterface, p, cert, secProtocols, null, false); - } - - m_server = server; - m_server.ServerName = string.Format("{0} v{1}", Library.AutoUpdater.AutoUpdateSettings.AppName, System.Reflection.Assembly.GetExecutingAssembly().GetName().Version); - this.Port = p; - - if (interfacestring != Program.DataConnection.ApplicationSettings.ServerListenInterface) - Program.DataConnection.ApplicationSettings.ServerListenInterface = interfacestring; - - if (certValid && !cert.Equals(Program.DataConnection.ApplicationSettings.ServerSSLCertificate)) - Program.DataConnection.ApplicationSettings.ServerSSLCertificate = cert; - - Duplicati.Library.Logging.Log.WriteInformationMessage(LOGTAG, "ServerListening", Strings.Server.StartedServer(listenInterface.ToString(), p)); - - return; - } - catch (System.Net.Sockets.SocketException) - { - } - - throw new Exception(Strings.Server.ServerStartFailure(ports)); - } - - private static void AddMimeTypes(FileModule fm) - { - fm.AddDefaultMimeTypes(); - fm.MimeTypes["htc"] = "text/x-component"; - fm.MimeTypes["json"] = "application/json"; - fm.MimeTypes["map"] = "application/json"; - fm.MimeTypes["htm"] = "text/html; charset=utf-8"; - fm.MimeTypes["html"] = "text/html; charset=utf-8"; - fm.MimeTypes["hbs"] = "application/x-handlebars-template"; - fm.MimeTypes["woff"] = "application/font-woff"; - fm.MimeTypes["woff2"] = "application/font-woff"; - } - - private static HttpServer.HttpServer CreateServer(IDictionary options) - { - HttpServer.HttpServer server = new HttpServer.HttpServer(); - - server.Add(new HostHeaderChecker()); - - if (string.Equals(Environment.GetEnvironmentVariable("SYNO_DSM_AUTH") ?? string.Empty, "1")) - server.Add(new SynologyAuthenticationHandler()); - - server.Add(new AuthenticationHandler()); - - server.Add(new RESTHandler()); - - string webroot = System.IO.Path.GetDirectoryName(System.Reflection.Assembly.GetExecutingAssembly().Location); - string install_webroot = System.IO.Path.Combine(Library.AutoUpdater.UpdaterManager.InstalledBaseDir, "webroot"); - -#if DEBUG - // Easy test for extensions while debugging - install_webroot = Library.AutoUpdater.UpdaterManager.InstalledBaseDir; - - if (!System.IO.Directory.Exists(System.IO.Path.Combine(webroot, "webroot"))) - { - //For debug we go "../../../.." to get out of "GUI/Duplicati.GUI.TrayIcon/bin/debug" - string tmpwebroot = System.IO.Path.GetFullPath(System.IO.Path.Combine(webroot, "..", "..", "..", "..")); - tmpwebroot = System.IO.Path.Combine(tmpwebroot, "Server"); - if (System.IO.Directory.Exists(System.IO.Path.Combine(tmpwebroot, "webroot"))) - webroot = tmpwebroot; - else - { - //If we are running the server standalone, we only need to exit "bin/Debug" - tmpwebroot = System.IO.Path.GetFullPath(System.IO.Path.Combine(webroot, "..", "..")); - if (System.IO.Directory.Exists(System.IO.Path.Combine(tmpwebroot, "webroot"))) - webroot = tmpwebroot; - } - } -#endif - - webroot = System.IO.Path.Combine(webroot, "webroot"); - - if (options.ContainsKey(OPTION_WEBROOT)) - { - string userroot = options[OPTION_WEBROOT]; -#if DEBUG - //In debug mode we do not care where the path points -#else - //In release mode we check that the user supplied path is located - // in the same folders as the running application, to avoid users - // that inadvertently expose top level folders - if (!string.IsNullOrWhiteSpace(userroot) - && - ( - userroot.StartsWith(Util.AppendDirSeparator(System.Reflection.Assembly.GetExecutingAssembly().Location), Library.Utility.Utility.ClientFilenameStringComparison) - || - userroot.StartsWith(Util.AppendDirSeparator(Program.StartupPath), Library.Utility.Utility.ClientFilenameStringComparison) - ) - ) -#endif - { - webroot = userroot; - install_webroot = webroot; - } - } - - if (install_webroot != webroot && System.IO.Directory.Exists(System.IO.Path.Combine(install_webroot, "customized"))) - { - var customized_files = new CacheControlFileHandler("/customized/", System.IO.Path.Combine(install_webroot, "customized")); - AddMimeTypes(customized_files); - server.Add(customized_files); - } - - if (install_webroot != webroot && System.IO.Directory.Exists(System.IO.Path.Combine(install_webroot, "oem"))) - { - var oem_files = new CacheControlFileHandler("/oem/", System.IO.Path.Combine(install_webroot, "oem")); - AddMimeTypes(oem_files); - server.Add(oem_files); - } - - if (install_webroot != webroot && System.IO.Directory.Exists(System.IO.Path.Combine(install_webroot, "package"))) - { - var proxy_files = new CacheControlFileHandler("/proxy/", System.IO.Path.Combine(install_webroot, "package")); - AddMimeTypes(proxy_files); - server.Add(proxy_files); - } - - var fh = new CacheControlFileHandler("/", webroot, true); - AddMimeTypes(fh); - server.Add(fh); - - server.Add(new IndexHtmlHandler(webroot)); -#if DEBUG - //For debugging, it is nice to know when we get a 404 - server.Add(new DebugReportHandler()); -#endif - return server; - } - - private class DebugReportHandler : HttpModule - { - public override bool Process(HttpServer.IHttpRequest request, HttpServer.IHttpResponse response, HttpServer.Sessions.IHttpSession session) - { - System.Diagnostics.Trace.WriteLine(string.Format("Rejecting request for {0}", request.Uri)); - return false; - } - } - - private class CacheControlFileHandler : FileModule - { - public CacheControlFileHandler(string baseUri, string basePath, bool useLastModifiedHeader = false) - : base(baseUri, basePath, useLastModifiedHeader) - { - - } - - public override bool Process(HttpServer.IHttpRequest request, HttpServer.IHttpResponse response, HttpServer.Sessions.IHttpSession session) - { - if (!this.CanHandle(request.Uri)) - return false; - - if (request.Uri.AbsolutePath.EndsWith("index.html", StringComparison.Ordinal) || request.Uri.AbsolutePath.EndsWith("index.htm", StringComparison.Ordinal)) - response.AddHeader("Cache-Control", "no-cache, no-store, must-revalidate, max-age=0"); - else - response.AddHeader("Cache-Control", "max-age=" + (60 * 60 * 24)); - return base.Process(request, response, session); - } - } - - /// - /// Module for injecting host header verification - /// - private class HostHeaderChecker : HttpModule - { - /// - /// The hostnames that we allow - /// - private string[] m_lastSplitNames; - - /// - /// The string used to generate m_lastSplitNames; - /// - private string m_lastAllowed; - - /// - /// A regex to detect potential IPv4 addresses. - /// Note that this also detects things that are not valid IPv4. - /// - private static readonly System.Text.RegularExpressions.Regex IPV4 = new System.Text.RegularExpressions.Regex(@"((\d){1,3}\.){3}(\d){1,3}"); - /// - /// A regex to detect potential IPv6 addresses. - /// Note that this also detects things that are not valid IPv6. - /// - private static readonly System.Text.RegularExpressions.Regex IPV6 = new System.Text.RegularExpressions.Regex(@"(\:)?(\:?[A-Fa-f0-9]{1,4}\:?){1,8}(\:)?"); - - /// - /// The hostnames that are always allowed - /// - private static readonly string[] DEFAULT_ALLOWED = new string[] { "localhost", "127.0.0.1", "::1", "localhost.localdomain" }; - - /// - /// Process the received request - /// - /// A flag indicating if the request is handled. - /// The received request. - /// The response object. - /// The session state. - public override bool Process(HttpServer.IHttpRequest request, HttpServer.IHttpResponse response, HttpServer.Sessions.IHttpSession session) - { - string[] h = null; - var hstring = Program.DataConnection.ApplicationSettings.AllowedHostnames; - - if (!string.IsNullOrWhiteSpace(hstring)) - { - h = m_lastSplitNames; - if (hstring != m_lastAllowed) - { - m_lastAllowed = hstring; - h = m_lastSplitNames = (hstring ?? string.Empty).Split(new char[] { ';' }, StringSplitOptions.RemoveEmptyEntries); - } - - if (h == null || h.Length == 0) - h = null; - } - - // For some reason, the web server strips out the host header - var host = request.Headers["Host"]; - if (string.IsNullOrWhiteSpace(host)) - host = request.Uri.Host; - - // This should not happen - if (string.IsNullOrWhiteSpace(host)) - { - response.Reason = "Invalid request, missing host header"; - response.Status = System.Net.HttpStatusCode.Forbidden; - var msg = System.Text.Encoding.ASCII.GetBytes(response.Reason); - response.ContentType = "text/plain"; - response.ContentLength = msg.Length; - response.Body.Write(msg, 0, msg.Length); - response.Send(); - return true; - } - - // Check the hostnames we always allow - if (DEFAULT_ALLOWED.Contains(host, StringComparer.OrdinalIgnoreCase)) - return false; - - // Then the user specified ones - if (h != null && h.Contains(host, StringComparer.OrdinalIgnoreCase)) - return false; - - // Disable checks if we have an asterisk - if (h != null && Array.IndexOf(h, "*") >= 0) - return false; - - // Finally, check if we have a potential IP address - var v4 = IPV4.Match(host); - var v6 = IPV6.Match(host); - - if ((v4.Success && v4.Length == host.Length) || (v6.Success && v6.Length == host.Length)) - { - try - { - // Verify that the hostname is indeed a valid IP address - System.Net.IPAddress.Parse(host); - return false; - } - catch - { } - } - - // Failed to find a valid header - response.Reason = $"The host header sent by the client is not allowed"; - response.Status = System.Net.HttpStatusCode.Forbidden; - var txt = System.Text.Encoding.ASCII.GetBytes(response.Reason); - response.ContentType = "text/plain"; - response.ContentLength = txt.Length; - response.Body.Write(txt, 0, txt.Length); - response.Send(); - return true; - - } - } - } -} +using System; +using System.Collections; +using System.Collections.Generic; +using System.Linq; +using HttpServer.HttpModules; +using System.Security.Cryptography.X509Certificates; +using Duplicati.Library.Common.IO; +using Duplicati.Library.RestAPI; + +namespace Duplicati.Server.WebServer +{ + public class Server + { + /// + /// The tag used for logging + /// + private static readonly string LOGTAG = Duplicati.Library.Logging.Log.LogTagFromType(); + + /// + /// Option for changing the webroot folder + /// + public const string OPTION_WEBROOT = "webservice-webroot"; + + /// + /// Option for changing the webservice listen port + /// + public const string OPTION_PORT = "webservice-port"; + + /// + /// Option for changing the webservice listen interface + /// + public const string OPTION_INTERFACE = "webservice-interface"; + + /// + /// The default path to the web root + /// + public const string DEFAULT_OPTION_WEBROOT = "webroot"; + + /// + /// The default listening port + /// + public const int DEFAULT_OPTION_PORT = 8200; + + /// + /// Option for setting the webservice SSL certificate + /// + public const string OPTION_SSLCERTIFICATEFILE = "webservice-sslcertificatefile"; + + /// + /// Option for setting the webservice SSL certificate key + /// + public const string OPTION_SSLCERTIFICATEFILEPASSWORD = "webservice-sslcertificatepassword"; + + /// + /// The default listening interface + /// + public const string DEFAULT_OPTION_INTERFACE = "loopback"; + + /// + /// The single webserver instance + /// + private readonly HttpServer.HttpServer m_server; + + /// + /// The webserver listening port + /// + public readonly int Port; + + /// + /// A string that is sent out instead of password values + /// + public const string PASSWORD_PLACEHOLDER = "**********"; + + /// + /// Sets up the webserver and starts it + /// + /// A set of options + public Server(IDictionary options) + { + string portstring; + IEnumerable ports = null; + options.TryGetValue(OPTION_PORT, out portstring); + if (!string.IsNullOrEmpty(portstring)) + ports = + from n in portstring.Split(new char[] { ',' }, StringSplitOptions.RemoveEmptyEntries) + where int.TryParse(n, out _) + select int.Parse(n); + + if (ports == null || !ports.Any()) + ports = new int[] { DEFAULT_OPTION_PORT }; + + string interfacestring; + System.Net.IPAddress listenInterface; + options.TryGetValue(OPTION_INTERFACE, out interfacestring); + + if (string.IsNullOrWhiteSpace(interfacestring)) + interfacestring = FIXMEGlobal.DataConnection.ApplicationSettings.ServerListenInterface; + if (string.IsNullOrWhiteSpace(interfacestring)) + interfacestring = DEFAULT_OPTION_INTERFACE; + + if (interfacestring.Trim() == "*" || interfacestring.Trim().Equals("any", StringComparison.OrdinalIgnoreCase) || interfacestring.Trim().Equals("all", StringComparison.OrdinalIgnoreCase)) + listenInterface = System.Net.IPAddress.Any; + else if (interfacestring.Trim() == "loopback") + listenInterface = System.Net.IPAddress.Loopback; + else + listenInterface = System.Net.IPAddress.Parse(interfacestring); + + string certificateFile; + options.TryGetValue(OPTION_SSLCERTIFICATEFILE, out certificateFile); + + string certificateFilePassword; + options.TryGetValue(OPTION_SSLCERTIFICATEFILEPASSWORD, out certificateFilePassword); + + X509Certificate2 cert = null; + bool certValid = false; + + if (certificateFile == null) + { + try + { + cert = FIXMEGlobal.DataConnection.ApplicationSettings.ServerSSLCertificate; + + if (cert != null) + certValid = cert.HasPrivateKey; + } + catch (Exception ex) + { + Duplicati.Library.Logging.Log.WriteWarningMessage(LOGTAG, "DefectStoredSSLCert", ex, Strings.Server.DefectSSLCertInDatabase); + } + } + else if (certificateFile.Length == 0) + { + FIXMEGlobal.DataConnection.ApplicationSettings.ServerSSLCertificate = null; + } + else + { + try + { + if (string.IsNullOrWhiteSpace(certificateFilePassword)) + cert = new X509Certificate2(certificateFile, "", X509KeyStorageFlags.Exportable); + else + cert = new X509Certificate2(certificateFile, certificateFilePassword, X509KeyStorageFlags.Exportable); + + certValid = cert.HasPrivateKey; + } + catch (Exception ex) + { + throw new Exception(Strings.Server.SSLCertificateFailure(ex.Message), ex); + } + } + + // If we are in hosted mode with no specified port, + // then try different ports + foreach (var p in ports) + try + { + // Due to the way the server is initialized, + // we cannot try to start it again on another port, + // so we create a new server for each attempt + + var server = CreateServer(options); + + if (!certValid) + server.Start(listenInterface, p); + else + { + var secProtocols = System.Security.Authentication.SslProtocols.Tls12; + + try + { + //try TLS 1.3 (type not available on .NET < 4.8) + secProtocols = System.Security.Authentication.SslProtocols.Tls12 | (System.Security.Authentication.SslProtocols)12288; + } + catch (NotSupportedException) + { + } + server.Start(listenInterface, p, cert, secProtocols, null, false); + } + + m_server = server; + m_server.ServerName = string.Format("{0} v{1}", Library.AutoUpdater.AutoUpdateSettings.AppName, System.Reflection.Assembly.GetExecutingAssembly().GetName().Version); + this.Port = p; + + if (interfacestring != FIXMEGlobal.DataConnection.ApplicationSettings.ServerListenInterface) + FIXMEGlobal.DataConnection.ApplicationSettings.ServerListenInterface = interfacestring; + + if (certValid && !cert.Equals(FIXMEGlobal.DataConnection.ApplicationSettings.ServerSSLCertificate)) + FIXMEGlobal.DataConnection.ApplicationSettings.ServerSSLCertificate = cert; + + Duplicati.Library.Logging.Log.WriteInformationMessage(LOGTAG, "ServerListening", Strings.Server.StartedServer(listenInterface.ToString(), p)); + + return; + } + catch (System.Net.Sockets.SocketException) + { + } + + throw new Exception(Strings.Server.ServerStartFailure(ports)); + } + + private static void AddMimeTypes(FileModule fm) + { + fm.AddDefaultMimeTypes(); + fm.MimeTypes["htc"] = "text/x-component"; + fm.MimeTypes["json"] = "application/json"; + fm.MimeTypes["map"] = "application/json"; + fm.MimeTypes["htm"] = "text/html; charset=utf-8"; + fm.MimeTypes["html"] = "text/html; charset=utf-8"; + fm.MimeTypes["hbs"] = "application/x-handlebars-template"; + fm.MimeTypes["woff"] = "application/font-woff"; + fm.MimeTypes["woff2"] = "application/font-woff"; + } + + private static HttpServer.HttpServer CreateServer(IDictionary options) + { + HttpServer.HttpServer server = new HttpServer.HttpServer(); + + server.Add(new HostHeaderChecker()); + + if (string.Equals(Environment.GetEnvironmentVariable("SYNO_DSM_AUTH") ?? string.Empty, "1")) + server.Add(new SynologyAuthenticationHandler()); + + server.Add(new AuthenticationHandler()); + + server.Add(new RESTHandler()); + + string webroot = System.IO.Path.GetDirectoryName(System.Reflection.Assembly.GetExecutingAssembly().Location); + string install_webroot = System.IO.Path.Combine(Library.AutoUpdater.UpdaterManager.InstalledBaseDir, "webroot"); + +#if DEBUG + // Easy test for extensions while debugging + install_webroot = Library.AutoUpdater.UpdaterManager.InstalledBaseDir; + + if (!System.IO.Directory.Exists(System.IO.Path.Combine(webroot, "webroot"))) + { + //For debug we go "../../../.." to get out of "GUI/Duplicati.GUI.TrayIcon/bin/debug" + string tmpwebroot = System.IO.Path.GetFullPath(System.IO.Path.Combine(webroot, "..", "..", "..", "..")); + tmpwebroot = System.IO.Path.Combine(tmpwebroot, "Server"); + if (System.IO.Directory.Exists(System.IO.Path.Combine(tmpwebroot, "webroot"))) + webroot = tmpwebroot; + else + { + //If we are running the server standalone, we only need to exit "bin/Debug" + tmpwebroot = System.IO.Path.GetFullPath(System.IO.Path.Combine(webroot, "..", "..")); + if (System.IO.Directory.Exists(System.IO.Path.Combine(tmpwebroot, "webroot"))) + webroot = tmpwebroot; + } + } +#endif + + webroot = System.IO.Path.Combine(webroot, "webroot"); + + if (options.ContainsKey(OPTION_WEBROOT)) + { + string userroot = options[OPTION_WEBROOT]; +#if DEBUG + //In debug mode we do not care where the path points +#else + //In release mode we check that the user supplied path is located + // in the same folders as the running application, to avoid users + // that inadvertently expose top level folders + if (!string.IsNullOrWhiteSpace(userroot) + && + ( + userroot.StartsWith(Util.AppendDirSeparator(System.Reflection.Assembly.GetExecutingAssembly().Location), Library.Utility.Utility.ClientFilenameStringComparison) + || + userroot.StartsWith(Util.AppendDirSeparator(Program.StartupPath), Library.Utility.Utility.ClientFilenameStringComparison) + ) + ) +#endif + { + webroot = userroot; + install_webroot = webroot; + } + } + + if (install_webroot != webroot && System.IO.Directory.Exists(System.IO.Path.Combine(install_webroot, "customized"))) + { + var customized_files = new CacheControlFileHandler("/customized/", System.IO.Path.Combine(install_webroot, "customized")); + AddMimeTypes(customized_files); + server.Add(customized_files); + } + + if (install_webroot != webroot && System.IO.Directory.Exists(System.IO.Path.Combine(install_webroot, "oem"))) + { + var oem_files = new CacheControlFileHandler("/oem/", System.IO.Path.Combine(install_webroot, "oem")); + AddMimeTypes(oem_files); + server.Add(oem_files); + } + + if (install_webroot != webroot && System.IO.Directory.Exists(System.IO.Path.Combine(install_webroot, "package"))) + { + var proxy_files = new CacheControlFileHandler("/proxy/", System.IO.Path.Combine(install_webroot, "package")); + AddMimeTypes(proxy_files); + server.Add(proxy_files); + } + + var fh = new CacheControlFileHandler("/", webroot, true); + AddMimeTypes(fh); + server.Add(fh); + + server.Add(new IndexHtmlHandler(webroot)); +#if DEBUG + //For debugging, it is nice to know when we get a 404 + server.Add(new DebugReportHandler()); +#endif + return server; + } + + private class DebugReportHandler : HttpModule + { + public override bool Process(HttpServer.IHttpRequest request, HttpServer.IHttpResponse response, HttpServer.Sessions.IHttpSession session) + { + System.Diagnostics.Trace.WriteLine(string.Format("Rejecting request for {0}", request.Uri)); + return false; + } + } + + private class CacheControlFileHandler : FileModule + { + public CacheControlFileHandler(string baseUri, string basePath, bool useLastModifiedHeader = false) + : base(baseUri, basePath, useLastModifiedHeader) + { + + } + + public override bool Process(HttpServer.IHttpRequest request, HttpServer.IHttpResponse response, HttpServer.Sessions.IHttpSession session) + { + if (!this.CanHandle(request.Uri)) + return false; + + if (request.Uri.AbsolutePath.EndsWith("index.html", StringComparison.Ordinal) || request.Uri.AbsolutePath.EndsWith("index.htm", StringComparison.Ordinal)) + response.AddHeader("Cache-Control", "no-cache, no-store, must-revalidate, max-age=0"); + else + response.AddHeader("Cache-Control", "max-age=" + (60 * 60 * 24)); + return base.Process(request, response, session); + } + } + + /// + /// Module for injecting host header verification + /// + private class HostHeaderChecker : HttpModule + { + /// + /// The hostnames that we allow + /// + private string[] m_lastSplitNames; + + /// + /// The string used to generate m_lastSplitNames; + /// + private string m_lastAllowed; + + /// + /// A regex to detect potential IPv4 addresses. + /// Note that this also detects things that are not valid IPv4. + /// + private static readonly System.Text.RegularExpressions.Regex IPV4 = new System.Text.RegularExpressions.Regex(@"((\d){1,3}\.){3}(\d){1,3}"); + /// + /// A regex to detect potential IPv6 addresses. + /// Note that this also detects things that are not valid IPv6. + /// + private static readonly System.Text.RegularExpressions.Regex IPV6 = new System.Text.RegularExpressions.Regex(@"(\:)?(\:?[A-Fa-f0-9]{1,4}\:?){1,8}(\:)?"); + + /// + /// The hostnames that are always allowed + /// + private static readonly string[] DEFAULT_ALLOWED = new string[] { "localhost", "127.0.0.1", "::1", "localhost.localdomain" }; + + /// + /// Process the received request + /// + /// A flag indicating if the request is handled. + /// The received request. + /// The response object. + /// The session state. + public override bool Process(HttpServer.IHttpRequest request, HttpServer.IHttpResponse response, HttpServer.Sessions.IHttpSession session) + { + string[] h = null; + var hstring = FIXMEGlobal.DataConnection.ApplicationSettings.AllowedHostnames; + + if (!string.IsNullOrWhiteSpace(hstring)) + { + h = m_lastSplitNames; + if (hstring != m_lastAllowed) + { + m_lastAllowed = hstring; + h = m_lastSplitNames = (hstring ?? string.Empty).Split(new char[] { ';' }, StringSplitOptions.RemoveEmptyEntries); + } + + if (h == null || h.Length == 0) + h = null; + } + + // For some reason, the web server strips out the host header + var host = request.Headers["Host"]; + if (string.IsNullOrWhiteSpace(host)) + host = request.Uri.Host; + + // This should not happen + if (string.IsNullOrWhiteSpace(host)) + { + response.Reason = "Invalid request, missing host header"; + response.Status = System.Net.HttpStatusCode.Forbidden; + var msg = System.Text.Encoding.ASCII.GetBytes(response.Reason); + response.ContentType = "text/plain"; + response.ContentLength = msg.Length; + response.Body.Write(msg, 0, msg.Length); + response.Send(); + return true; + } + + // Check the hostnames we always allow + if (DEFAULT_ALLOWED.Contains(host, StringComparer.OrdinalIgnoreCase)) + return false; + + // Then the user specified ones + if (h != null && h.Contains(host, StringComparer.OrdinalIgnoreCase)) + return false; + + // Disable checks if we have an asterisk + if (h != null && Array.IndexOf(h, "*") >= 0) + return false; + + // Finally, check if we have a potential IP address + var v4 = IPV4.Match(host); + var v6 = IPV6.Match(host); + + if ((v4.Success && v4.Length == host.Length) || (v6.Success && v6.Length == host.Length)) + { + try + { + // Verify that the hostname is indeed a valid IP address + System.Net.IPAddress.Parse(host); + return false; + } + catch + { } + } + + // Failed to find a valid header + response.Reason = $"The host header sent by the client is not allowed"; + response.Status = System.Net.HttpStatusCode.Forbidden; + var txt = System.Text.Encoding.ASCII.GetBytes(response.Reason); + response.ContentType = "text/plain"; + response.ContentLength = txt.Length; + response.Body.Write(txt, 0, txt.Length); + response.Send(); + return true; + + } + } + } +} diff --git a/Duplicati/Server/WebServer/SynologyAuthenticationHandler.cs b/Duplicati.Library.RestAPI/WebServer/SynologyAuthenticationHandler.cs similarity index 97% rename from Duplicati/Server/WebServer/SynologyAuthenticationHandler.cs rename to Duplicati.Library.RestAPI/WebServer/SynologyAuthenticationHandler.cs index 77fef2a19..33639591b 100644 --- a/Duplicati/Server/WebServer/SynologyAuthenticationHandler.cs +++ b/Duplicati.Library.RestAPI/WebServer/SynologyAuthenticationHandler.cs @@ -1,289 +1,289 @@ -// Copyright (C) 2017, The Duplicati Team -// http://www.duplicati.com, info@duplicati.com -// -// This library is free software; you can redistribute it and/or modify -// it under the terms of the GNU Lesser General Public License as -// published by the Free Software Foundation; either version 2.1 of the -// License, or (at your option) any later version. -// -// This library is distributed in the hope that it will be useful, but -// WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU -// Lesser General Public License for more details. -// -// You should have received a copy of the GNU Lesser General Public -// License along with this library; if not, write to the Free Software -// Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA -using System; -using System.Collections.Concurrent; -using System.Collections.Generic; -using System.Diagnostics; -using System.IO; -using System.Linq; -using System.Text.RegularExpressions; -using System.Threading.Tasks; -using HttpServer.HttpModules; - -namespace Duplicati.Server.WebServer -{ - /// - /// Helper class for enforcing the built-in authentication on Synology DSM - /// - public class SynologyAuthenticationHandler : HttpModule - { - /// - /// The path to the login.cgi script - /// - private readonly string LOGIN_CGI = GetEnvArg("SYNO_LOGIN_CGI", "/usr/syno/synoman/webman/login.cgi"); - /// - /// The path to the authenticate.cgi script - /// - private readonly string AUTH_CGI = GetEnvArg("SYNO_AUTHENTICATE_CGI", "/usr/syno/synoman/webman/modules/authenticate.cgi"); - /// - /// A flag indicating if only admins are allowed - /// - private readonly bool ADMIN_ONLY = !(GetEnvArg("SYNO_ALL_USERS", "0") == "1"); - /// - /// A flag indicating if the XSRF token should be fetched automatically - /// - private readonly bool AUTO_XSRF = GetEnvArg("SYNO_AUTO_XSRF", "1") == "1"; - - /// - /// A flag indicating that the auth-module is fully disabled - /// - private readonly bool FULLY_DISABLED; - - /// - /// Re-evaluate the logins periodically to ensure it is still valid - /// - private readonly TimeSpan CACHE_TIMEOUT = TimeSpan.FromMinutes(3); - - /// - /// A cache of previously authenticated logins - /// - private readonly ConcurrentDictionary m_logincache = new ConcurrentDictionary(); - - /// - /// Initializes a new instance of the class. - /// - public SynologyAuthenticationHandler() - { - Console.WriteLine("Enabling Synology integrated authentication handler"); - var disable = false; - if (!File.Exists(LOGIN_CGI)) - { - Console.WriteLine("Disabling webserver as the login script is not found: {0}", LOGIN_CGI); - disable = true; - } - if (!File.Exists(AUTH_CGI)) - { - Console.WriteLine("Disabling webserver as the auth script is not found: {0}", AUTH_CGI); - disable = true; - } - - FULLY_DISABLED = disable; - } - - /// - /// Processes the request - /// - /// true if the request is handled false otherwise. - /// The request. - /// The response. - /// The session. - public override bool Process(HttpServer.IHttpRequest request, HttpServer.IHttpResponse response, HttpServer.Sessions.IHttpSession session) - { - if (FULLY_DISABLED) - { - response.Status = System.Net.HttpStatusCode.ServiceUnavailable; - response.Reason = "The system is incorrectly configured"; - return true; - } - - var limitedAccess = - request.Uri.AbsolutePath.StartsWith(RESTHandler.API_URI_PATH, StringComparison.OrdinalIgnoreCase) - || - request.Uri.AbsolutePath.StartsWith(AuthenticationHandler.LOGIN_SCRIPT_URI, StringComparison.OrdinalIgnoreCase) - || - request.Uri.AbsolutePath.StartsWith(AuthenticationHandler.LOGOUT_SCRIPT_URI, StringComparison.OrdinalIgnoreCase); - - if (!limitedAccess) - return false; - - var tmpenv = new Dictionary(); - - tmpenv["REMOTE_ADDR"] = request.RemoteEndPoint.Address.ToString(); - tmpenv["REMOTE_PORT"] = request.RemoteEndPoint.Port.ToString(); - - if (!string.IsNullOrWhiteSpace(request.Headers["X-Real-IP"])) - tmpenv["REMOTE_ADDR"] = request.Headers["X-Real-IP"]; - if (!string.IsNullOrWhiteSpace(request.Headers["X-Real-IP"])) - tmpenv["REMOTE_PORT"] = request.Headers["X-Real-Port"]; - - var loginid = request.Cookies["id"]?.Value; - if (!string.IsNullOrWhiteSpace(loginid)) - tmpenv["HTTP_COOKIE"] = "id=" + loginid; - - var xsrftoken = request.Headers["X-Syno-Token"]; - if (string.IsNullOrWhiteSpace(xsrftoken)) - xsrftoken = request.QueryString["SynoToken"]?.Value; - - var cachestring = BuildCacheKey(tmpenv, xsrftoken); - - DateTime cacheExpires; - if (m_logincache.TryGetValue(cachestring, out cacheExpires) && cacheExpires > DateTime.Now) - { - // We do not refresh the cache, as we need to ask the synology auth system periodically - return false; - } - - if (string.IsNullOrWhiteSpace(xsrftoken) && AUTO_XSRF) - { - var authre = new Regex(@"""SynoToken""\s?\:\s?""(?[^""]+)"""); - try - { - var resp = ShellExec(LOGIN_CGI, env: tmpenv).Result; - - var m = authre.Match(resp); - if (m.Success) - xsrftoken = m.Groups["token"].Value; - else - throw new Exception("Unable to get XSRF token"); - } - catch (Exception) - { - response.Status = System.Net.HttpStatusCode.InternalServerError; - response.Reason = "The system is incorrectly configured"; - return true; - - } - } - - if (!string.IsNullOrWhiteSpace(xsrftoken)) - tmpenv["HTTP_X_SYNO_TOKEN"] = xsrftoken; - - cachestring = BuildCacheKey(tmpenv, xsrftoken); - - var username = GetEnvArg("SYNO_USERNAME"); - if (string.IsNullOrWhiteSpace(username)) - { - try - { - username = ShellExec(AUTH_CGI, shell: false, exitcode: 0, env: tmpenv).Result; - } - catch (Exception) - { - response.Status = System.Net.HttpStatusCode.InternalServerError; - response.Reason = "The system is incorrectly configured"; - return true; - } - } - - if (string.IsNullOrWhiteSpace(username)) - { - response.Status = System.Net.HttpStatusCode.Forbidden; - response.Reason = "Permission denied, not logged in"; - return true; - } - - username = username.Trim(); - - if (ADMIN_ONLY) - { - var groups = GetEnvArg("SYNO_GROUP_IDS"); - - if (string.IsNullOrWhiteSpace(groups)) - { - groups = ShellExec("id", "-G '" + username.Trim().Replace("'", "\\'") + "'", exitcode: 0).Result ?? string.Empty; - groups = groups.Replace(Environment.NewLine, String.Empty); - } - - if (!groups.Split(new char[] { ' ' }).Contains("101")) - { - response.Status = System.Net.HttpStatusCode.Forbidden; - response.Reason = "Administrator login required"; - return true; - } - } - - // We are now authenticated, add to cache - m_logincache[cachestring] = DateTime.Now + CACHE_TIMEOUT; - return false; - } - - /// - /// Builds a cache key from the environment data - /// - /// The cache key. - /// The environment. - /// The XSRF token. - private static string BuildCacheKey(Dictionary values, string xsrftoken) - { - if (!values.ContainsKey("REMOTE_ADDR") || !values.ContainsKey("REMOTE_PORT") || !values.ContainsKey("HTTP_COOKIE")) - return null; - - return string.Format("{0}:{1}/{2}?{3}", values["REMOTE_ADDR"], values["REMOTE_PORT"], values["HTTP_COOKIE"], xsrftoken); - } - - /// - /// Runs an external command - /// - /// The stdout data. - /// The executable - /// The executable and the arguments. - /// If set to true use the shell context for execution. - /// Set the value to check for a particular exitcode. - private static async Task ShellExec(string command, string args = null, bool shell = false, int exitcode = -1, Dictionary env = null) - { - var psi = new ProcessStartInfo() - { - FileName = command, - Arguments = shell ? null : args, - UseShellExecute = false, - RedirectStandardInput = shell, - RedirectStandardOutput = true, - RedirectStandardError = false - }; - - if (env != null) - foreach (var pk in env) - psi.EnvironmentVariables[pk.Key] = pk.Value; - - using (var p = System.Diagnostics.Process.Start(psi)) - { - if (shell && args != null) - await p.StandardInput.WriteLineAsync(args); - - var res = p.StandardOutput.ReadToEndAsync(); - - var tries = 10; - var ms = (int)TimeSpan.FromSeconds(0.5).TotalMilliseconds; - while (tries > 0 && !p.HasExited) - { - tries--; - p.WaitForExit(ms); - } - - if (!p.HasExited) - try { p.Kill(); } - catch { } - - if (!p.HasExited || (p.ExitCode != exitcode && exitcode != -1)) - throw new Exception(string.Format("Exit code was: {0}, stdout: {1}", p.ExitCode, res)); - return await res; - } - } - - /// - /// Gets the environment variable argument. - /// - /// The environment variable. - /// The name of the environment variable. - /// The default value. - private static string GetEnvArg(string key, string @default = null) - { - var res = Environment.GetEnvironmentVariable(key); - return string.IsNullOrWhiteSpace(res) ? @default : res.Trim(); - } - } -} +// Copyright (C) 2017, The Duplicati Team +// http://www.duplicati.com, info@duplicati.com +// +// This library is free software; you can redistribute it and/or modify +// it under the terms of the GNU Lesser General Public License as +// published by the Free Software Foundation; either version 2.1 of the +// License, or (at your option) any later version. +// +// This library is distributed in the hope that it will be useful, but +// WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +// Lesser General Public License for more details. +// +// You should have received a copy of the GNU Lesser General Public +// License along with this library; if not, write to the Free Software +// Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA +using System; +using System.Collections.Concurrent; +using System.Collections.Generic; +using System.Diagnostics; +using System.IO; +using System.Linq; +using System.Text.RegularExpressions; +using System.Threading.Tasks; +using HttpServer.HttpModules; + +namespace Duplicati.Server.WebServer +{ + /// + /// Helper class for enforcing the built-in authentication on Synology DSM + /// + public class SynologyAuthenticationHandler : HttpModule + { + /// + /// The path to the login.cgi script + /// + private readonly string LOGIN_CGI = GetEnvArg("SYNO_LOGIN_CGI", "/usr/syno/synoman/webman/login.cgi"); + /// + /// The path to the authenticate.cgi script + /// + private readonly string AUTH_CGI = GetEnvArg("SYNO_AUTHENTICATE_CGI", "/usr/syno/synoman/webman/modules/authenticate.cgi"); + /// + /// A flag indicating if only admins are allowed + /// + private readonly bool ADMIN_ONLY = !(GetEnvArg("SYNO_ALL_USERS", "0") == "1"); + /// + /// A flag indicating if the XSRF token should be fetched automatically + /// + private readonly bool AUTO_XSRF = GetEnvArg("SYNO_AUTO_XSRF", "1") == "1"; + + /// + /// A flag indicating that the auth-module is fully disabled + /// + private readonly bool FULLY_DISABLED; + + /// + /// Re-evaluate the logins periodically to ensure it is still valid + /// + private readonly TimeSpan CACHE_TIMEOUT = TimeSpan.FromMinutes(3); + + /// + /// A cache of previously authenticated logins + /// + private readonly ConcurrentDictionary m_logincache = new ConcurrentDictionary(); + + /// + /// Initializes a new instance of the class. + /// + public SynologyAuthenticationHandler() + { + Console.WriteLine("Enabling Synology integrated authentication handler"); + var disable = false; + if (!File.Exists(LOGIN_CGI)) + { + Console.WriteLine("Disabling webserver as the login script is not found: {0}", LOGIN_CGI); + disable = true; + } + if (!File.Exists(AUTH_CGI)) + { + Console.WriteLine("Disabling webserver as the auth script is not found: {0}", AUTH_CGI); + disable = true; + } + + FULLY_DISABLED = disable; + } + + /// + /// Processes the request + /// + /// true if the request is handled false otherwise. + /// The request. + /// The response. + /// The session. + public override bool Process(HttpServer.IHttpRequest request, HttpServer.IHttpResponse response, HttpServer.Sessions.IHttpSession session) + { + if (FULLY_DISABLED) + { + response.Status = System.Net.HttpStatusCode.ServiceUnavailable; + response.Reason = "The system is incorrectly configured"; + return true; + } + + var limitedAccess = + request.Uri.AbsolutePath.StartsWith(RESTHandler.API_URI_PATH, StringComparison.OrdinalIgnoreCase) + || + request.Uri.AbsolutePath.StartsWith(AuthenticationHandler.LOGIN_SCRIPT_URI, StringComparison.OrdinalIgnoreCase) + || + request.Uri.AbsolutePath.StartsWith(AuthenticationHandler.LOGOUT_SCRIPT_URI, StringComparison.OrdinalIgnoreCase); + + if (!limitedAccess) + return false; + + var tmpenv = new Dictionary(); + + tmpenv["REMOTE_ADDR"] = request.RemoteEndPoint.Address.ToString(); + tmpenv["REMOTE_PORT"] = request.RemoteEndPoint.Port.ToString(); + + if (!string.IsNullOrWhiteSpace(request.Headers["X-Real-IP"])) + tmpenv["REMOTE_ADDR"] = request.Headers["X-Real-IP"]; + if (!string.IsNullOrWhiteSpace(request.Headers["X-Real-IP"])) + tmpenv["REMOTE_PORT"] = request.Headers["X-Real-Port"]; + + var loginid = request.Cookies["id"]?.Value; + if (!string.IsNullOrWhiteSpace(loginid)) + tmpenv["HTTP_COOKIE"] = "id=" + loginid; + + var xsrftoken = request.Headers["X-Syno-Token"]; + if (string.IsNullOrWhiteSpace(xsrftoken)) + xsrftoken = request.QueryString["SynoToken"]?.Value; + + var cachestring = BuildCacheKey(tmpenv, xsrftoken); + + DateTime cacheExpires; + if (m_logincache.TryGetValue(cachestring, out cacheExpires) && cacheExpires > DateTime.Now) + { + // We do not refresh the cache, as we need to ask the synology auth system periodically + return false; + } + + if (string.IsNullOrWhiteSpace(xsrftoken) && AUTO_XSRF) + { + var authre = new Regex(@"""SynoToken""\s?\:\s?""(?[^""]+)"""); + try + { + var resp = ShellExec(LOGIN_CGI, env: tmpenv).Result; + + var m = authre.Match(resp); + if (m.Success) + xsrftoken = m.Groups["token"].Value; + else + throw new Exception("Unable to get XSRF token"); + } + catch (Exception) + { + response.Status = System.Net.HttpStatusCode.InternalServerError; + response.Reason = "The system is incorrectly configured"; + return true; + + } + } + + if (!string.IsNullOrWhiteSpace(xsrftoken)) + tmpenv["HTTP_X_SYNO_TOKEN"] = xsrftoken; + + cachestring = BuildCacheKey(tmpenv, xsrftoken); + + var username = GetEnvArg("SYNO_USERNAME"); + if (string.IsNullOrWhiteSpace(username)) + { + try + { + username = ShellExec(AUTH_CGI, shell: false, exitcode: 0, env: tmpenv).Result; + } + catch (Exception) + { + response.Status = System.Net.HttpStatusCode.InternalServerError; + response.Reason = "The system is incorrectly configured"; + return true; + } + } + + if (string.IsNullOrWhiteSpace(username)) + { + response.Status = System.Net.HttpStatusCode.Forbidden; + response.Reason = "Permission denied, not logged in"; + return true; + } + + username = username.Trim(); + + if (ADMIN_ONLY) + { + var groups = GetEnvArg("SYNO_GROUP_IDS"); + + if (string.IsNullOrWhiteSpace(groups)) + { + groups = ShellExec("id", "-G '" + username.Trim().Replace("'", "\\'") + "'", exitcode: 0).Result ?? string.Empty; + groups = groups.Replace(Environment.NewLine, String.Empty); + } + + if (!groups.Split(new char[] { ' ' }).Contains("101")) + { + response.Status = System.Net.HttpStatusCode.Forbidden; + response.Reason = "Administrator login required"; + return true; + } + } + + // We are now authenticated, add to cache + m_logincache[cachestring] = DateTime.Now + CACHE_TIMEOUT; + return false; + } + + /// + /// Builds a cache key from the environment data + /// + /// The cache key. + /// The environment. + /// The XSRF token. + private static string BuildCacheKey(Dictionary values, string xsrftoken) + { + if (!values.ContainsKey("REMOTE_ADDR") || !values.ContainsKey("REMOTE_PORT") || !values.ContainsKey("HTTP_COOKIE")) + return null; + + return string.Format("{0}:{1}/{2}?{3}", values["REMOTE_ADDR"], values["REMOTE_PORT"], values["HTTP_COOKIE"], xsrftoken); + } + + /// + /// Runs an external command + /// + /// The stdout data. + /// The executable + /// The executable and the arguments. + /// If set to true use the shell context for execution. + /// Set the value to check for a particular exitcode. + private static async Task ShellExec(string command, string args = null, bool shell = false, int exitcode = -1, Dictionary env = null) + { + var psi = new ProcessStartInfo() + { + FileName = command, + Arguments = shell ? null : args, + UseShellExecute = false, + RedirectStandardInput = shell, + RedirectStandardOutput = true, + RedirectStandardError = false + }; + + if (env != null) + foreach (var pk in env) + psi.EnvironmentVariables[pk.Key] = pk.Value; + + using (var p = System.Diagnostics.Process.Start(psi)) + { + if (shell && args != null) + await p.StandardInput.WriteLineAsync(args); + + var res = p.StandardOutput.ReadToEndAsync(); + + var tries = 10; + var ms = (int)TimeSpan.FromSeconds(0.5).TotalMilliseconds; + while (tries > 0 && !p.HasExited) + { + tries--; + p.WaitForExit(ms); + } + + if (!p.HasExited) + try { p.Kill(); } + catch { } + + if (!p.HasExited || (p.ExitCode != exitcode && exitcode != -1)) + throw new Exception(string.Format("Exit code was: {0}, stdout: {1}", p.ExitCode, res)); + return await res; + } + } + + /// + /// Gets the environment variable argument. + /// + /// The environment variable. + /// The name of the environment variable. + /// The default value. + private static string GetEnvArg(string key, string @default = null) + { + var res = Environment.GetEnvironmentVariable(key); + return string.IsNullOrWhiteSpace(res) ? @default : res.Trim(); + } + } +} diff --git a/Duplicati/Server/newbackup.json b/Duplicati.Library.RestAPI/newbackup.json similarity index 96% rename from Duplicati/Server/newbackup.json rename to Duplicati.Library.RestAPI/newbackup.json index 97c35f4fa..3fbb3c65b 100644 --- a/Duplicati/Server/newbackup.json +++ b/Duplicati.Library.RestAPI/newbackup.json @@ -1,15 +1,15 @@ -{ - Backup: { - Settings: [ - {Name: "encryption-module", Value: "aes"}, - {Name: "compression-module", Value: "zip"}, - {Name: "dblock-size", Value: "50mb"}, - {Name: "keep-time", Value: ""} - ] - }, - Schedule: { - Repeat: "1D", - Time: "13:00:00", - AllowedDays: ["mon", "tue", "wed", "thu", "fri", "sat", "sun"] - } +{ + Backup: { + Settings: [ + {Name: "encryption-module", Value: "aes"}, + {Name: "compression-module", Value: "zip"}, + {Name: "dblock-size", Value: "50mb"}, + {Name: "keep-time", Value: ""} + ] + }, + Schedule: { + Repeat: "1D", + Time: "13:00:00", + AllowedDays: ["mon", "tue", "wed", "thu", "fri", "sat", "sun"] + } } \ No newline at end of file diff --git a/Duplicati.sln b/Duplicati.sln index 5891e7de5..e0d67a087 100644 --- a/Duplicati.sln +++ b/Duplicati.sln @@ -154,6 +154,10 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Implementation", "Implement EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Duplicati.Library.Backends", "Duplicati\Library\Backends\Duplicati.Library.Backends.csproj", "{5290E237-C2CD-48F2-99D2-817F9C2163C8}" EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Duplicati.WebserverCore", "Duplicati\WebserverCore\Duplicati.WebserverCore.csproj", "{5A702CEE-DB36-4153-BD94-D8CF867E75A9}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Duplicati.Library.RestAPI", "Duplicati.Library.RestAPI\Duplicati.Library.RestAPI.csproj", "{C1D4D665-23A3-4216-9CD1-D67AE9AAAA4C}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -424,6 +428,14 @@ Global {5290E237-C2CD-48F2-99D2-817F9C2163C8}.Debug|Any CPU.Build.0 = Debug|Any CPU {5290E237-C2CD-48F2-99D2-817F9C2163C8}.Release|Any CPU.ActiveCfg = Release|Any CPU {5290E237-C2CD-48F2-99D2-817F9C2163C8}.Release|Any CPU.Build.0 = Release|Any CPU + {5A702CEE-DB36-4153-BD94-D8CF867E75A9}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {5A702CEE-DB36-4153-BD94-D8CF867E75A9}.Debug|Any CPU.Build.0 = Debug|Any CPU + {5A702CEE-DB36-4153-BD94-D8CF867E75A9}.Release|Any CPU.ActiveCfg = Release|Any CPU + {5A702CEE-DB36-4153-BD94-D8CF867E75A9}.Release|Any CPU.Build.0 = Release|Any CPU + {C1D4D665-23A3-4216-9CD1-D67AE9AAAA4C}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {C1D4D665-23A3-4216-9CD1-D67AE9AAAA4C}.Debug|Any CPU.Build.0 = Debug|Any CPU + {C1D4D665-23A3-4216-9CD1-D67AE9AAAA4C}.Release|Any CPU.ActiveCfg = Release|Any CPU + {C1D4D665-23A3-4216-9CD1-D67AE9AAAA4C}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -445,6 +457,7 @@ Global {17566860-3D98-4604-AA5B-47661F75609F} = {D19A38DD-68F1-4EF5-BF5F-8966CE0D9A5B} {2AF960C0-357D-4D44-A3D5-8B6E89DB0F11} = {D19A38DD-68F1-4EF5-BF5F-8966CE0D9A5B} {2C838169-B187-4B09-8768-1C24C2521C8D} = {566EBBDA-19A4-4056-A615-D901D57D2439} + {E93F3DE2-FF3A-4709-96A3-8190AA14FA25} = {D19A38DD-68F1-4EF5-BF5F-8966CE0D9A5B} {8E4CECFB-0413-4B00-AB93-78D1C3902BD5} = {E1A9B303-F281-45C5-A4F6-CADD9DE3F3C4} {5489181D-950C-44AF-873C-45EB0A3B6BD2} = {E1A9B303-F281-45C5-A4F6-CADD9DE3F3C4} {D9E4E686-423C-48EC-A392-404E7C00860C} = {E1A9B303-F281-45C5-A4F6-CADD9DE3F3C4} diff --git a/Duplicati/CommandLine/ConfigurationImporter/Duplicati.CommandLine.ConfigurationImporter.csproj b/Duplicati/CommandLine/ConfigurationImporter/Duplicati.CommandLine.ConfigurationImporter.csproj index 8f2ef415d..f05705b68 100644 --- a/Duplicati/CommandLine/ConfigurationImporter/Duplicati.CommandLine.ConfigurationImporter.csproj +++ b/Duplicati/CommandLine/ConfigurationImporter/Duplicati.CommandLine.ConfigurationImporter.csproj @@ -1,7 +1,7 @@  - netstandard2.0 + net6.0 LGPL, Copyright © Duplicati Team 2021 Duplicati.CommandLine.ConfigurationImporter.Implementation Duplicati.CommandLine.ConfigurationImporter diff --git a/Duplicati/Server/Duplicati.Server.csproj b/Duplicati/Server/Duplicati.Server.csproj index 39b012ba0..de5b79361 100644 --- a/Duplicati/Server/Duplicati.Server.csproj +++ b/Duplicati/Server/Duplicati.Server.csproj @@ -1,7 +1,7 @@  - netstandard2.0 + net6.0 LGPL, Copyright © Duplicati Team 2021 The Server Duplicati implementation Duplicati.Server.Implementation @@ -19,6 +19,7 @@ + @@ -45,6 +46,7 @@ + @@ -69,11 +71,6 @@ - - - - - Always diff --git a/Duplicati/Server/Program.cs b/Duplicati/Server/Program.cs index a905d5968..146f6c11d 100644 --- a/Duplicati/Server/Program.cs +++ b/Duplicati/Server/Program.cs @@ -4,6 +4,8 @@ using System.Globalization; using System.Linq; using Duplicati.Library.Common; using Duplicati.Library.Common.IO; +using Duplicati.Library.RestAPI; +using Duplicati.WebserverCore; namespace Duplicati.Server { @@ -36,7 +38,7 @@ namespace Duplicati.Server /// /// Gets the folder where Duplicati data is stored /// - public static string DataFolder { get; private set; } + public static string DataFolder { get => FIXMEGlobal.DataFolder; private set => FIXMEGlobal.DataFolder = value; } /// /// The single instance @@ -46,27 +48,27 @@ namespace Duplicati.Server /// /// This is the only access to the database /// - public static Database.Connection DataConnection; + public static Database.Connection DataConnection { get => FIXMEGlobal.DataConnection; set => FIXMEGlobal.DataConnection = value; } /// /// This is the lock to be used before manipulating the shared resources /// - public static readonly object MainLock = new object(); + public static object MainLock { get => FIXMEGlobal.MainLock; } /// /// This is the scheduling thread /// - public static Scheduler Scheduler; + public static Scheduler Scheduler { get => FIXMEGlobal.Scheduler; set => FIXMEGlobal.Scheduler = value; } /// /// This is the working thread /// - public static Duplicati.Library.Utility.WorkerThread WorkThread; + public static Duplicati.Library.Utility.WorkerThread WorkThread { get => FIXMEGlobal.WorkThread; set => FIXMEGlobal.WorkThread = value; } /// /// List of completed task results /// - public static readonly List> TaskResultCache = new List>(); + public static List> TaskResultCache { get => FIXMEGlobal.TaskResultCache; } /// /// The maximum number of completed task results to keep in memory @@ -86,22 +88,27 @@ namespace Duplicati.Server /// /// The controller interface for pause/resume and throttle options /// - public static LiveControls LiveControl; + public static LiveControls LiveControl { get => FIXMEGlobal.LiveControl; set => FIXMEGlobal.LiveControl = value; } /// /// The application exit event /// - public static System.Threading.ManualResetEvent ApplicationExitEvent; + public static System.Threading.ManualResetEvent ApplicationExitEvent { get => FIXMEGlobal.ApplicationExitEvent; set => FIXMEGlobal.ApplicationExitEvent = value; } /// /// The webserver instance /// private static WebServer.Server WebServer; + /// + /// Callback to shutdown the modern webserver + /// + private static Action ShutdownModernWebserver; + /// /// The update poll thread. /// - public static UpdatePollThread UpdatePoller; + public static UpdatePollThread UpdatePoller { get => FIXMEGlobal.UpdatePoller; set => FIXMEGlobal.UpdatePoller = value; } /// /// An event that is set once the server is ready to respond to requests @@ -111,12 +118,12 @@ namespace Duplicati.Server /// /// The status event signaler, used to control long polling of status updates /// - public static readonly EventPollNotify StatusEventNotifyer = new EventPollNotify(); + public static EventPollNotify StatusEventNotifyer { get => FIXMEGlobal.StatusEventNotifyer; } /// /// A delegate method for creating a copy of the current progress state /// - public static Func GenerateProgressState; + public static Func GenerateProgressState { get => FIXMEGlobal.GenerateProgressState; set => FIXMEGlobal.GenerateProgressState = value; } /// /// An event ID that increases whenever the database is updated @@ -131,12 +138,12 @@ namespace Duplicati.Server /// /// The log redirect handler /// - public static readonly LogWriteHandler LogHandler = new LogWriteHandler(); + public static LogWriteHandler LogHandler { get => FIXMEGlobal.LogHandler; } /// /// Used to check the origin of the web server (e.g. Tray icon or a stand alone Server) /// - public static string Origin = "Server"; + public static string Origin { get => FIXMEGlobal.Origin; set => FIXMEGlobal.Origin = value; } private static System.Threading.Timer PurgeTempFilesTimer = null; @@ -166,6 +173,27 @@ namespace Duplicati.Server set { DataConnection.ApplicationSettings.ServerPortChanged = value; } } + public static void IncrementLastDataUpdateID() + { + System.Threading.Interlocked.Increment(ref Program.LastDataUpdateID); + } + + public static void IncrementLastNotificationUpdateID() + { + System.Threading.Interlocked.Increment(ref Program.LastNotificationUpdateID); + } + + static Program() + { + FIXMEGlobal.IncrementLastDataUpdateID = Program.IncrementLastDataUpdateID; + FIXMEGlobal.PeekLastDataUpdateID = () => Program.LastDataUpdateID; + FIXMEGlobal.IncrementLastNotificationUpdateID = Program.IncrementLastNotificationUpdateID; + FIXMEGlobal.PeekLastNotificationUpdateID = () => Program.LastNotificationUpdateID; + FIXMEGlobal.GetDatabaseConnection = Program.GetDatabaseConnection; + FIXMEGlobal.StartOrStopUsageReporter = Program.StartOrStopUsageReporter; + FIXMEGlobal.UpdateThrottleSpeeds = Program.UpdateThrottleSpeeds; + } + /// /// The main entry point for the application. /// @@ -291,6 +319,7 @@ namespace Duplicati.Server { StatusEventNotifyer.SignalNewEvent(); + ShutdownModernWebserver(); UpdatePoller?.Terminate(); Scheduler?.Terminate(true); WorkThread?.Terminate(true); @@ -317,6 +346,9 @@ namespace Duplicati.Server ServerPortChanged |= WebServer.Port != DataConnection.ApplicationSettings.LastWebserverPort; DataConnection.ApplicationSettings.LastWebserverPort = WebServer.Port; + + var server = new DuplicatiWebserver(); + ShutdownModernWebserver = server.Foo(); } private static void SetWorkerThread() @@ -647,7 +679,7 @@ namespace Duplicati.Server //Attempt to open the database, handling any encryption present Duplicati.Library.SQLiteHelper.SQLiteLoader.OpenDatabase(con, DatabasePath, useDatabaseEncryption, dbPassword); - Duplicati.Library.SQLiteHelper.DatabaseUpgrader.UpgradeDatabase(con, DatabasePath, typeof(Database.Connection)); + Duplicati.Library.SQLiteHelper.DatabaseUpgrader.UpgradeDatabase(con, DatabasePath, typeof(Duplicati.Library.RestAPI.Database.DatabaseConnectionSchemaMarker)); } catch (Exception ex) { diff --git a/Duplicati/Service/Duplicati.Service.csproj b/Duplicati/Service/Duplicati.Service.csproj index 72ac037d3..ce324aee2 100644 --- a/Duplicati/Service/Duplicati.Service.csproj +++ b/Duplicati/Service/Duplicati.Service.csproj @@ -1,7 +1,7 @@  - netstandard2.0 + net6.0 LGPL, Copyright © Duplicati Team 2021 Duplicati.Service.Implementation diff --git a/Duplicati/WebserverCore/Duplicati.WebserverCore.csproj b/Duplicati/WebserverCore/Duplicati.WebserverCore.csproj new file mode 100644 index 000000000..cbcd65f1d --- /dev/null +++ b/Duplicati/WebserverCore/Duplicati.WebserverCore.csproj @@ -0,0 +1,15 @@ + + + + net6.0 + Library + enable + enable + true + + + + + + + diff --git a/Duplicati/WebserverCore/DuplicatiWebserver.cs b/Duplicati/WebserverCore/DuplicatiWebserver.cs new file mode 100644 index 000000000..7dc7cc37b --- /dev/null +++ b/Duplicati/WebserverCore/DuplicatiWebserver.cs @@ -0,0 +1,14 @@ +namespace Duplicati.WebserverCore +{ + public class DuplicatiWebserver + { + public Action Foo() { + var builder = WebApplication.CreateBuilder(); + var app = builder.Build(); + + app.UseTestMiddleware(); + app.RunAsync("http://localhost:3001"); + return () => { app.StopAsync(); }; + } + } +} \ No newline at end of file diff --git a/Duplicati/WebserverCore/RestHandlerCore.cs b/Duplicati/WebserverCore/RestHandlerCore.cs new file mode 100644 index 000000000..c7c46252a --- /dev/null +++ b/Duplicati/WebserverCore/RestHandlerCore.cs @@ -0,0 +1,56 @@ +// Copyright (C) 2023, The Duplicati Team +// http://www.duplicati.com, info@duplicati.com +// +// This library is free software; you can redistribute it and/or modify +// it under the terms of the GNU Lesser General Public License as +// published by the Free Software Foundation; either version 2.1 of the +// License, or (at your option) any later version. +// +// This library is distributed in the hope that it will be useful, but +// WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +// Lesser General Public License for more details. +// +// You should have received a copy of the GNU Lesser General Public +// License along with this library; if not, write to the Free Software +// Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + +using Duplicati.Server.WebServer.RESTMethods; + +namespace Duplicati.WebserverCorer +{ + public class RESTHandlerCore + { + public const string API_URI_PATH = "/api/v1"; + public static readonly int API_URI_SEGMENTS = API_URI_PATH.Split(new char[] { '/' }).Length; + + private static readonly Dictionary _modules = new Dictionary(StringComparer.OrdinalIgnoreCase); + + public static IDictionary Modules { get { return _modules; } } + + /// + /// Loads all REST modules in the Duplicati.Server.WebServer.RESTMethods namespace + /// + static RESTHandlerCore() + { + var lst = + from n in typeof(IRESTMethod).Assembly.GetTypes() + where + n.Namespace == typeof(IRESTMethod).Namespace + && + typeof(IRESTMethod).IsAssignableFrom(n) + && + !n.IsAbstract + && + !n.IsInterface + select n; + + foreach (var t in lst) + { + var m = (IRESTMethod)Activator.CreateInstance(t); + _modules.Add(t.Name.ToLowerInvariant(), m); + } + } + } +} + diff --git a/Duplicati/WebserverCore/TestMiddleware.cs b/Duplicati/WebserverCore/TestMiddleware.cs new file mode 100644 index 000000000..04c8a9246 --- /dev/null +++ b/Duplicati/WebserverCore/TestMiddleware.cs @@ -0,0 +1,37 @@ +using System.Globalization; +namespace Duplicati.WebserverCore +{ + public class TestMiddleware + { + private readonly RequestDelegate _next; + + public TestMiddleware(RequestDelegate next) + { + _next = next; + } + + public async Task InvokeAsync(HttpContext context) + { + var cultureQuery = context.Request.Query["culture"]; + if (!string.IsNullOrWhiteSpace(cultureQuery)) + { + var culture = new CultureInfo(cultureQuery); + + CultureInfo.CurrentCulture = culture; + CultureInfo.CurrentUICulture = culture; + } + + // Call the next delegate/middleware in the pipeline. + await _next(context); + } + } + + public static class TestMiddlewareExtensions + { + public static IApplicationBuilder UseTestMiddleware( + this IApplicationBuilder builder) + { + return builder.UseMiddleware(); + } + } +} \ No newline at end of file