diff --git a/Duplicati/Library/Backend/GoogleServices/GCSConfig.cs b/Duplicati/Library/Backend/GoogleServices/GCSConfig.cs index f6296db0e..d51a730ed 100644 --- a/Duplicati/Library/Backend/GoogleServices/GCSConfig.cs +++ b/Duplicati/Library/Backend/GoogleServices/GCSConfig.cs @@ -1,4 +1,4 @@ -// Copyright (C) 2015, The Duplicati Team +// 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 @@ -19,7 +19,7 @@ using System; using Duplicati.Library.Interface; using System.Collections.Generi namespace Duplicati.Library.Backend.GoogleServices { public class GCSConfig : IWebModule - { private const ConfigType DEFAULT_CONFIG_TYPE = ConfigType.Locations; private static readonly string DEFAULT_CONFIG_TYPE_STR = Enum.GetName(typeof(ConfigType), DEFAULT_CONFIG_TYPE); private const string KEY_CONFIGTYPE = "gcs-config"; public enum ConfigType { Locations, StorageClasses } + { private const ConfigType DEFAULT_CONFIG_TYPE = ConfigType.Locations; private static readonly string DEFAULT_CONFIG_TYPE_STR = Enum.GetName(typeof(ConfigType), DEFAULT_CONFIG_TYPE); private const string KEY_CONFIGTYPE = "gcs-config"; public enum ConfigType { Locations, StorageClasses } public GCSConfig() { } diff --git a/Duplicati/Library/Backend/GoogleServices/GoogleCloudStorage.cs b/Duplicati/Library/Backend/GoogleServices/GoogleCloudStorage.cs index 28c3fd747..b6f59594e 100644 --- a/Duplicati/Library/Backend/GoogleServices/GoogleCloudStorage.cs +++ b/Duplicati/Library/Backend/GoogleServices/GoogleCloudStorage.cs @@ -1,4 +1,4 @@ -// Copyright (C) 2015, The Duplicati Team +// 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 @@ -19,10 +19,10 @@ using System; using Duplicati.Library.Interface; using System.Collections.Generi namespace Duplicati.Library.Backend.GoogleCloudStorage { public class GoogleCloudStorage : IBackend, IStreamingBackend, IRenameEnabledBackend - { private const string AUTHID_OPTION = "authid"; private const string PROJECT_OPTION = "gcs-project"; private const string LOCATION_OPTION = "gcs-location"; private const string STORAGECLASS_OPTION = "gcs-storage-class"; private const string API_URL = "https://www.googleapis.com/storage/v1"; private const string UPLOAD_API_URL = "https://www.googleapis.com/upload/storage/v1"; // From: https://cloud.google.com/storage/docs/bucket-locations public static readonly KeyValuePair[] KNOWN_GCS_LOCATIONS = new KeyValuePair[] { new KeyValuePair("(default)", null), new KeyValuePair("Europe", "EU"), new KeyValuePair("United States", "US"), new KeyValuePair("Asia", "ASIA"), //Regional buckets: https://cloud.google.com/storage/docs/regional-buckets new KeyValuePair("Eastern Asia-Pacific", "ASIA-EAST1"), new KeyValuePair("Central United States 1", "US-CENTRAL1"), new KeyValuePair("Central United States 2", "US-CENTRAL2"), new KeyValuePair("Eastern United States 1", "US-EAST1"), new KeyValuePair("Eastern United States 2", "US-EAST2"), new KeyValuePair("Eastern United States 3", "US-EAST3"), new KeyValuePair("Western United States", "US-WEST1"), }; public static readonly KeyValuePair[] KNOWN_GCS_STORAGE_CLASSES = new KeyValuePair[] { new KeyValuePair("(default)", null), new KeyValuePair("Standard", "STANDARD"), new KeyValuePair("Durable Reduced Availability (DRA)", "DURABLE_REDUCED_AVAILABILITY"), new KeyValuePair("Nearline", "NEARLINE"), }; private string m_bucket; private string m_prefix; private string m_project; private OAuthHelper m_oauth; private string m_location; private string m_storage_class; + { private const string AUTHID_OPTION = "authid"; private const string PROJECT_OPTION = "gcs-project"; private const string LOCATION_OPTION = "gcs-location"; private const string STORAGECLASS_OPTION = "gcs-storage-class"; private const string API_URL = "https://www.googleapis.com/storage/v1"; private const string UPLOAD_API_URL = "https://www.googleapis.com/upload/storage/v1"; // From: https://cloud.google.com/storage/docs/bucket-locations public static readonly KeyValuePair[] KNOWN_GCS_LOCATIONS = new KeyValuePair[] { new KeyValuePair("(default)", null), new KeyValuePair("Europe", "EU"), new KeyValuePair("United States", "US"), new KeyValuePair("Asia", "ASIA"), //Regional buckets: https://cloud.google.com/storage/docs/regional-buckets new KeyValuePair("Eastern Asia-Pacific", "ASIA-EAST1"), new KeyValuePair("Central United States 1", "US-CENTRAL1"), new KeyValuePair("Central United States 2", "US-CENTRAL2"), new KeyValuePair("Eastern United States 1", "US-EAST1"), new KeyValuePair("Eastern United States 2", "US-EAST2"), new KeyValuePair("Eastern United States 3", "US-EAST3"), new KeyValuePair("Western United States", "US-WEST1"), }; public static readonly KeyValuePair[] KNOWN_GCS_STORAGE_CLASSES = new KeyValuePair[] { new KeyValuePair("(default)", null), new KeyValuePair("Standard", "STANDARD"), new KeyValuePair("Durable Reduced Availability (DRA)", "DURABLE_REDUCED_AVAILABILITY"), new KeyValuePair("Nearline", "NEARLINE"), }; private string m_bucket; private string m_prefix; private string m_project; private OAuthHelper m_oauth; private string m_location; private string m_storage_class; public GoogleCloudStorage() { - } public GoogleCloudStorage(string url, Dictionary options) { var uri = new Utility.Uri(url); m_bucket = uri.Host; m_prefix = "/" + uri.Path; if (!m_prefix.EndsWith("/")) m_prefix += "/"; // For GCS we do not use a leading slash if (m_prefix.StartsWith("/")) m_prefix = m_prefix.Substring(1); string authid; options.TryGetValue(AUTHID_OPTION, out authid); options.TryGetValue(PROJECT_OPTION, out m_project); options.TryGetValue(LOCATION_OPTION, out m_location); options.TryGetValue(STORAGECLASS_OPTION, out m_storage_class); if (string.IsNullOrEmpty(authid)) throw new Exception(Strings.GoogleCloudStorage.MissingAuthID(AUTHID_OPTION)); m_oauth = new OAuthHelper(authid, this.ProtocolKey); m_oauth.AutoAuthHeader = true; } private class ListBucketResponse { public string kind { get; set; } public string nextPageToken { get; set; } public string[] prefixes { get; set; } public BucketResourceItem[] items { get; set; } } private class BucketResourceItem { public string kind { get; set; } public string id { get; set; } public string selfLink { get; set; } public string name { get; set; } public string contentType { get; set; } public DateTime? updated { get; set; } public string storageClass { get; set; } public long? size { get; set; } public string md5Hash { get; set; } public string mediaLink { get; set; } } private class CreateBucketRequest { public string name { get; set; } public string location { get; set; } public string storageClass { get; set; } } + } public GoogleCloudStorage(string url, Dictionary options) { var uri = new Utility.Uri(url); m_bucket = uri.Host; m_prefix = "/" + uri.Path; if (!m_prefix.EndsWith("/")) m_prefix += "/"; // For GCS we do not use a leading slash if (m_prefix.StartsWith("/")) m_prefix = m_prefix.Substring(1); string authid; options.TryGetValue(AUTHID_OPTION, out authid); options.TryGetValue(PROJECT_OPTION, out m_project); options.TryGetValue(LOCATION_OPTION, out m_location); options.TryGetValue(STORAGECLASS_OPTION, out m_storage_class); if (string.IsNullOrEmpty(authid)) throw new Exception(Strings.GoogleCloudStorage.MissingAuthID(AUTHID_OPTION)); m_oauth = new OAuthHelper(authid, this.ProtocolKey); m_oauth.AutoAuthHeader = true; } private class ListBucketResponse { public string kind { get; set; } public string nextPageToken { get; set; } public string[] prefixes { get; set; } public BucketResourceItem[] items { get; set; } } private class BucketResourceItem { public string kind { get; set; } public string id { get; set; } public string selfLink { get; set; } public string name { get; set; } public string contentType { get; set; } public DateTime? updated { get; set; } public string storageClass { get; set; } public long? size { get; set; } public string md5Hash { get; set; } public string mediaLink { get; set; } } private class CreateBucketRequest { public string name { get; set; } public string location { get; set; } public string storageClass { get; set; } } #region IBackend implementation public List List() { try { var res = new List(); string token = null; do { var url = string.Format("{0}/b/{1}/o?prefix={2}", API_URL, m_bucket, Library.Utility.Uri.UrlEncode(m_prefix)); if (!string.IsNullOrEmpty(token)) url += string.Format("&pageToken={0}", token); var resp = m_oauth.ReadJSONResponse(url); if (resp.items != null) foreach(var f in resp.items) { var name = f.name; if (name.StartsWith(m_prefix, StringComparison.InvariantCultureIgnoreCase)) name = name.Substring(m_prefix.Length); if (f.size == null) res.Add(new FileEntry(name)); else if (f.updated == null) res.Add(new FileEntry(name, f.size.Value)); else res.Add(new FileEntry(name, f.size.Value, f.updated.Value, f.updated.Value)); } token = resp.nextPageToken; } while(!string.IsNullOrEmpty(token)); return res; } catch (WebException wex) { if (wex.Response is HttpWebResponse && ((HttpWebResponse)wex.Response).StatusCode == HttpStatusCode.NotFound) throw new FolderMissingException(); else throw; } } public void Put(string remotename, string filename) { using (System.IO.FileStream fs = System.IO.File.OpenRead(filename)) Put(remotename, fs); } public void Get(string remotename, string filename) { using (System.IO.FileStream fs = System.IO.File.Create(filename)) Get(remotename, fs); } public void Delete(string remotename) { var url = string.Format("{0}/b/{1}/o/{2}", API_URL, m_bucket, Library.Utility.Uri.UrlPathEncode(m_prefix + remotename)); var req = m_oauth.CreateRequest(url); req.Method = "DELETE"; m_oauth.ReadJSONResponse(req); } public void Test() { List(); } public void CreateFolder() { if (string.IsNullOrEmpty(m_project)) throw new Exception(Strings.GoogleCloudStorage.ProjectIDMissingError(PROJECT_OPTION)); var data = System.Text.Encoding.UTF8.GetBytes(JsonConvert.SerializeObject(new CreateBucketRequest() { name = m_bucket, location = m_location, storageClass = m_storage_class })); var url = string.Format("{0}/b?project={1}", API_URL, m_project); var req = m_oauth.CreateRequest(url); req.Method = "POST"; req.ContentLength = data.Length; req.ContentType = "application/json; charset=UTF-8"; var areq = new AsyncHttpRequest(req); using(var rs = areq.GetRequestStream()) rs.Write(data, 0, data.Length); m_oauth.ReadJSONResponse(areq); } public string DisplayName { get { return Strings.GoogleCloudStorage.DisplayName; } } public string ProtocolKey { get { return "gcs"; } } public IList SupportedCommands { get { StringBuilder locations = new StringBuilder(); StringBuilder storageClasses = new StringBuilder(); foreach (KeyValuePair s in KNOWN_GCS_LOCATIONS) locations.AppendLine(string.Format("{0}: {1}", s.Key, s.Value)); foreach (KeyValuePair s in KNOWN_GCS_STORAGE_CLASSES) storageClasses.AppendLine(string.Format("{0}: {1}", s.Key, s.Value)); return new List(new ICommandLineArgument[] { new CommandLineArgument(LOCATION_OPTION, CommandLineArgument.ArgumentType.String, Strings.GoogleCloudStorage.LocationDescriptionShort, Strings.GoogleCloudStorage.LocationDescriptionLong(locations.ToString())), new CommandLineArgument(STORAGECLASS_OPTION, CommandLineArgument.ArgumentType.String, Strings.GoogleCloudStorage.StorageclassDescriptionShort, Strings.GoogleCloudStorage.StorageclassDescriptionLong(locations.ToString())), new CommandLineArgument(AUTHID_OPTION, CommandLineArgument.ArgumentType.Password, Strings.GoogleCloudStorage.AuthidShort, Strings.GoogleCloudStorage.AuthidLong(OAuthHelper.OAUTH_LOGIN_URL("gcs"))), new CommandLineArgument(PROJECT_OPTION, CommandLineArgument.ArgumentType.String, Strings.GoogleCloudStorage.ProjectDescriptionShort, Strings.GoogleCloudStorage.ProjectDescriptionLong), }); } } public string Description { get { return Strings.GoogleCloudStorage.Description; } } #endregion public void Put(string remotename, System.IO.Stream stream) { var url = string.Format("{0}/b/{1}/o?uploadType=resumable", UPLOAD_API_URL, m_bucket); var item = new BucketResourceItem() { name = m_prefix + remotename }; var res = GoogleCommon.ChunckedUploadWithResume(m_oauth, item, url, stream); if (res == null) throw new Exception(string.Format("Upload succeeded, but no data was returned")); } public void Get(string remotename, System.IO.Stream stream) { try { var url = string.Format("{0}/b/{1}/o/{2}?alt=media", API_URL, m_bucket, Library.Utility.Uri.UrlPathEncode(m_prefix + remotename)); var req = m_oauth.CreateRequest(url); var areq = new AsyncHttpRequest(req); using(var resp = areq.GetResponse()) using(var rs = resp.GetResponseStream()) Library.Utility.Utility.CopyStream(rs, stream); } catch (WebException wex) { if (wex.Response is HttpWebResponse && ((HttpWebResponse)wex.Response).StatusCode == HttpStatusCode.NotFound) throw new FileMissingException(); else throw; } } public void Rename(string oldname, string newname) { var data = System.Text.Encoding.UTF8.GetBytes(JsonConvert.SerializeObject(new BucketResourceItem() { name = m_prefix + newname, })); var url = string.Format("{0}/b/{1}/o/{2}", API_URL, m_bucket, Library.Utility.Uri.UrlPathEncode(m_prefix + oldname)); var req = m_oauth.CreateRequest(url); req.Method = "PATCH"; req.ContentLength = data.Length; req.ContentType = "application/json; charset=UTF-8"; var areq = new AsyncHttpRequest(req); using(var rs = areq.GetRequestStream()) rs.Write(data, 0, data.Length); m_oauth.ReadJSONResponse(req); } #region IDisposable implementation public void Dispose() { } #endregion } } diff --git a/Duplicati/Library/Backend/GoogleServices/GoogleCommon.cs b/Duplicati/Library/Backend/GoogleServices/GoogleCommon.cs index dd33823b3..be58a5e5d 100644 --- a/Duplicati/Library/Backend/GoogleServices/GoogleCommon.cs +++ b/Duplicati/Library/Backend/GoogleServices/GoogleCommon.cs @@ -1,4 +1,4 @@ -// Copyright (C) 2015, The Duplicati Team +// 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 diff --git a/Duplicati/Library/Backend/GoogleServices/GoogleDrive.cs b/Duplicati/Library/Backend/GoogleServices/GoogleDrive.cs index 5f4203aea..b635c42c6 100644 --- a/Duplicati/Library/Backend/GoogleServices/GoogleDrive.cs +++ b/Duplicati/Library/Backend/GoogleServices/GoogleDrive.cs @@ -1,4 +1,4 @@ -// Copyright (C) 2015, The Duplicati Team +// 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 @@ -19,7 +19,7 @@ using System; using System.Linq; using Duplicati.Library.Interface; using Duplic namespace Duplicati.Library.Backend.GoogleDrive { public class GoogleDrive : IBackend, IStreamingBackend, IQuotaEnabledBackend, IRenameEnabledBackend - { private const string AUTHID_OPTION = "authid"; private const string FOLDER_MIMETYPE = "application/vnd.google-apps.folder"; private const string DRIVE_API_UPLOAD_URL = "https://www.googleapis.com/upload/drive/v2"; private const string DRIVE_API_URL = "https://www.googleapis.com/drive/v2"; private string m_path; private OAuthHelper m_oauth; private string m_currentFolderId; private Dictionary m_filecache; public GoogleDrive() { } + { private const string AUTHID_OPTION = "authid"; private const string FOLDER_MIMETYPE = "application/vnd.google-apps.folder"; private const string DRIVE_API_UPLOAD_URL = "https://www.googleapis.com/upload/drive/v2"; private const string DRIVE_API_URL = "https://www.googleapis.com/drive/v2"; private string m_path; private OAuthHelper m_oauth; private string m_currentFolderId; private Dictionary m_filecache; public GoogleDrive() { } public GoogleDrive(string url, Dictionary options) { var uri = new Utility.Uri(url); m_path = uri.HostAndPath; if (!m_path.EndsWith("/")) m_path += "/"; string authid = null; if (options.ContainsKey(AUTHID_OPTION)) authid = options[AUTHID_OPTION]; m_oauth = new OAuthHelper(authid, this.ProtocolKey) { AutoAuthHeader = true }; m_filecache = new Dictionary(); } private string GetFolderId(string path, bool autocreate = false) { var curparent = GetAboutInfo().rootFolderId; var curdisplay = "/"; foreach(var p in path.Split(new char[] {'/'}, StringSplitOptions.RemoveEmptyEntries)) { var res = ListFolder(curparent, true, p).ToArray(); if (res.Length == 0) { if (!autocreate) throw new FolderMissingException(); curparent = CreateFolder(p, curparent).id; } else if (res.Length > 1) throw new Exception(Strings.GoogleDrive.MultipleEntries(p, curdisplay)); else curparent = res[0].id; curdisplay += p + "/"; } return curparent; } private string CurrentFolderId { get { if (string.IsNullOrEmpty(m_currentFolderId)) m_currentFolderId = GetFolderId(m_path); return m_currentFolderId; } } private string GetFileId(string remotename, bool throwMissingException = true) { string entry; m_filecache.TryGetValue(remotename, out entry); if (!string.IsNullOrEmpty(entry)) return entry; var list = ListFolder(CurrentFolderId, false, remotename).ToArray(); if (list.Length == 0) { if (throwMissingException) throw new FileMissingException(); else return null; } else if (list.Length > 1) throw new Exception(string.Format(Strings.GoogleDrive.MultipleEntries(remotename, m_path))); return m_filecache[remotename] = list[0].id; } private static string EscapeTitleEntries(string title) { return title.Replace("'", "\\'"); } #region IStreamingBackend implementation public void Put(string remotename, System.IO.Stream stream) { try { // Figure out if we update or create the file if (m_filecache.Count == 0) List(); string fileid; m_filecache.TryGetValue(remotename, out fileid); var isUpdate = !string.IsNullOrEmpty(fileid); var url = isUpdate ? string.Format("{0}/files/{1}?uploadType=resumable", DRIVE_API_UPLOAD_URL, Library.Utility.Uri.UrlPathEncode(fileid)) : string.Format("{0}/files?uploadType=resumable", DRIVE_API_UPLOAD_URL); var item = new GoogleDriveFolderItem() { title = remotename, description = remotename, mimeType = "application/octet-stream", labels = new GoogleDriveFolderItemLabels { hidden = true }, parents = new GoogleDriveParentReference[] { new GoogleDriveParentReference() { id = CurrentFolderId } } }; var res = GoogleCommon.ChunckedUploadWithResume(m_oauth, item, url, stream, isUpdate ? "PUT" : "POST"); m_filecache[remotename] = res.id; } catch { m_filecache.Clear(); throw; } } public void Get(string remotename, System.IO.Stream stream) { // Prevent repeated download url lookups if (m_filecache.Count == 0) List(); var fileid = GetFileId(remotename); var req = m_oauth.CreateRequest(string.Format("{0}/files/{1}?alt=media", DRIVE_API_URL, fileid)); var areq = new AsyncHttpRequest(req); using(var resp = (HttpWebResponse)areq.GetResponse()) using(var rs = resp.GetResponseStream()) Duplicati.Library.Utility.Utility.CopyStream(rs, stream); } #endregion #region IBackend implementation public List List() { try { var res = new List(); m_filecache.Clear(); foreach(var n in ListFolder(CurrentFolderId)) { FileEntry fe = null; if (n.fileSize == null) fe = new FileEntry(n.title); else if (n.modifiedDate == null) fe = new FileEntry(n.title, n.fileSize.Value); else fe = new FileEntry(n.title, n.fileSize.Value, n.modifiedDate.Value, n.modifiedDate.Value); if (fe != null) { fe.IsFolder = FOLDER_MIMETYPE.Equals(n.mimeType, StringComparison.InvariantCultureIgnoreCase); res.Add(fe); if (!fe.IsFolder) m_filecache[fe.Name] = n.id; } } return res; } catch { m_filecache.Clear(); throw; } } public void Put(string remotename, string filename) { using (System.IO.FileStream fs = System.IO.File.OpenRead(filename)) Put(remotename, fs); } public void Get(string remotename, string filename) { using (System.IO.FileStream fs = System.IO.File.Create(filename)) Get(remotename, fs); } public void Delete(string remotename) { try { // Prevent repeated fileid lookups if (m_filecache.Count == 0) List(); var fileid = GetFileId(remotename); var url = string.Format("{0}/files/{1}", DRIVE_API_URL, Library.Utility.Uri.UrlPathEncode(fileid)); m_oauth.GetJSONData(url, x => { x.Method = "DELETE"; }); m_filecache.Remove(remotename); } catch { m_filecache.Clear(); throw; } } public void Test() { List(); } public void CreateFolder() { m_filecache.Clear(); m_currentFolderId = GetFolderId(m_path, true); } public string DisplayName { get { return Strings.GoogleDrive.DisplayName; } } public string ProtocolKey { get { return "googledrive"; } } public System.Collections.Generic.IList SupportedCommands { get { return new List(new ICommandLineArgument[] { new CommandLineArgument(AUTHID_OPTION, CommandLineArgument.ArgumentType.Password, Strings.GoogleDrive.AuthidShort, Strings.GoogleDrive.AuthidLong(OAuthHelper.OAUTH_LOGIN_URL("googledrive"))), }); } } public string Description { get { return Strings.GoogleDrive.Description; } } #endregion #region IQuotaEnabledBackend implementation public long TotalQuotaSpace { get { try { return GetAboutInfo().quotaBytesTotal ?? -1; } catch { } return -1; } } public long FreeQuotaSpace { get { try { return GetAboutInfo().quotaBytesUsed ?? -1; } catch { } return -1; } } #endregion #region IRenameEnabledBackend implementation public void Rename(string oldname, string newname) { try { var files = ListFolder(CurrentFolderId, false, oldname).ToArray(); if (files.Length == 0) throw new FileMissingException(); else if (files.Length > 1) throw new Exception(string.Format(Strings.GoogleDrive.MultipleEntries(oldname, m_path))); var newfile = JsonConvert.DeserializeObject(JsonConvert.SerializeObject(files[0])); newfile.title = newname; newfile.parents = new GoogleDriveParentReference[] { new GoogleDriveParentReference() { id = CurrentFolderId } }; var url = string.Format("{0}/files/{1}", DRIVE_API_UPLOAD_URL, Library.Utility.Uri.UrlPathEncode(files[0].id)); var data = System.Text.Encoding.UTF8.GetBytes(JsonConvert.SerializeObject(newfile)); var nf = m_oauth.GetJSONData(url, x => { x.Method = "PUT"; x.ContentLength = data.Length; x.ContentType = "application/json; charset=UTF-8"; }, x => { using(var rs = x.GetRequestStream()) rs.Write(data, 0, data.Length); }); m_filecache[newname] = nf.id; m_filecache.Remove(oldname); } catch { m_filecache.Clear(); throw; } } #endregion #region IDisposable implementation public void Dispose() { } #endregion private class GoogleDriveParentReference { public string kind { get; set; } public string id { get; set; } public string selfLink { get; set; } public string parentLink { get; set; } public bool? isRoot { get; set; } } private class GoogleDriveListResponse { public string kind { get; set; } public string etag { get; set; } public string selfLink { get; set; } public string nextPageToken { get; set; } public string nextLink { get; set; } public GoogleDriveFolderItem[] items { get; set; } } private class GoogleDriveFolderItemLabels { public bool starred { get; set; } public bool hidden { get; set; } public bool thrashed { get; set; } public bool restricted { get; set; } public bool viewed { get; set; } } private class GoogleDriveFolderItem { public string kind { get; set; } public string id { get; set; } public string etag { get; set; } public string selfLink { get; set; } public string title { get; set; } public string description { get; set; } public string mimeType { get; set; } public GoogleDriveFolderItemLabels labels { get; set; } public DateTime? createdDate { get; set; } public DateTime? modifiedDate { get; set; } public string downloadUrl { get; set; } public string originalFilename { get; set; } public string md5Checksum { get; set; } public long? fileSize { get; set; } public long? quotaBytesUsed { get; set; } public GoogleDriveParentReference[] parents { get; set; } } private class GoogleDriveAboutResponse { public string kind { get; set; } public string etag { get; set; } public string selfLink { get; set; } public string name { get; set; } public long? quotaBytesTotal { get; set; } public long? quotaBytesUsed { get; set; } public long? quotaBytesUsedAggregate { get; set; } public long? quotaBytesUsedInTrash { get; set; } public string quotaType { get; set; } public string rootFolderId { get; set; } } private IEnumerable ListFolder(string parentfolder, bool? onlyFolders = null, string name = null) { var p = new string[] { string.IsNullOrEmpty(name) ? null : string.Format("title = '{0}'", EscapeTitleEntries(name)), onlyFolders == null ? null : string.Format("mimeType {0}= '{1}'", onlyFolders.Value ? "" : "!", FOLDER_MIMETYPE), string.Format("'{0}' in parents", EscapeTitleEntries(parentfolder)) }; var url = string.Format("{0}/files?q={1}", DRIVE_API_URL, Library.Utility.Uri.UrlEncode(string.Join(" and ", p.Where(x => x != null)))); while (!string.IsNullOrEmpty(url)) { var res = m_oauth.GetJSONData(url); foreach(var n in res.items) yield return n; url = res.nextLink; } } private GoogleDriveAboutResponse GetAboutInfo() { var url = string.Format("{0}/about", DRIVE_API_URL); return m_oauth.GetJSONData(url); } private GoogleDriveFolderItem CreateFolder(string name, string parent) { var url = string.Format("{0}/files", DRIVE_API_URL); var folder = new GoogleDriveFolderItem() { title = name, description = name, mimeType = FOLDER_MIMETYPE, labels = new GoogleDriveFolderItemLabels { hidden = true }, parents = new GoogleDriveParentReference[] { new GoogleDriveParentReference() { id = parent } } }; var data = System.Text.Encoding.UTF8.GetBytes(JsonConvert.SerializeObject(folder)); return m_oauth.GetJSONData(url, x => { x.Method = "POST"; x.ContentType = "application/json; charset=UTF-8"; x.ContentLength = data.Length; }, req => { using(var rs = req.GetRequestStream()) rs.Write(data, 0, data.Length); }); } } diff --git a/Duplicati/Library/Backend/GoogleServices/Properties/AssemblyInfo.cs b/Duplicati/Library/Backend/GoogleServices/Properties/AssemblyInfo.cs index dedc38ee1..3ca81bfa6 100644 --- a/Duplicati/Library/Backend/GoogleServices/Properties/AssemblyInfo.cs +++ b/Duplicati/Library/Backend/GoogleServices/Properties/AssemblyInfo.cs @@ -1,4 +1,4 @@ -// Copyright (C) 2015, The Duplicati Team +// 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 diff --git a/Duplicati/Library/Backend/GoogleServices/Strings.cs b/Duplicati/Library/Backend/GoogleServices/Strings.cs index 0e3d2c430..a5e78d68a 100644 --- a/Duplicati/Library/Backend/GoogleServices/Strings.cs +++ b/Duplicati/Library/Backend/GoogleServices/Strings.cs @@ -1,4 +1,4 @@ -// Copyright (C) 2015, The Duplicati Team +// 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 diff --git a/Duplicati/Library/Backend/HubiC/HubiCBackend.cs b/Duplicati/Library/Backend/HubiC/HubiCBackend.cs index 36fcabc27..748842d59 100644 --- a/Duplicati/Library/Backend/HubiC/HubiCBackend.cs +++ b/Duplicati/Library/Backend/HubiC/HubiCBackend.cs @@ -1,4 +1,4 @@ -// Copyright (C) 2015, The Duplicati Team +// 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 diff --git a/Duplicati/Library/Backend/HubiC/Properties/AssemblyInfo.cs b/Duplicati/Library/Backend/HubiC/Properties/AssemblyInfo.cs index 84dbb57a1..8d40ead11 100644 --- a/Duplicati/Library/Backend/HubiC/Properties/AssemblyInfo.cs +++ b/Duplicati/Library/Backend/HubiC/Properties/AssemblyInfo.cs @@ -1,4 +1,4 @@ -// Copyright (C) 2015, The Duplicati Team +// 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 diff --git a/Duplicati/Library/Backend/HubiC/Strings.cs b/Duplicati/Library/Backend/HubiC/Strings.cs index ce5cb5b1f..3bdf8bdda 100644 --- a/Duplicati/Library/Backend/HubiC/Strings.cs +++ b/Duplicati/Library/Backend/HubiC/Strings.cs @@ -1,4 +1,4 @@ -// Copyright (C) 2015, The Duplicati Team +// 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 @@ -13,7 +13,7 @@ // // 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.Localization.Short; +// Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA using Duplicati.Library.Localization.Short; namespace Duplicati.Library.Backend.Strings { internal static class HubiC { public static string Description { get { return LC.L(@"This backend can read and write data to HubiC. Supported format is ""hubic://container/folder""."); } } public static string DisplayName { get { return LC.L(@"HubiC"); } } public static string MissingAuthID(string url) { return LC.L(@"You need an AuthID, you can get it from: {0}"); } public static string AuthidShort { get { return LC.L(@"The authorization code"); } } public static string AuthidLong(string url) { return LC.L(@"The authorization token retrieved from {0}", url); } } } diff --git a/Duplicati/Library/Backend/OAuthHelper/JSONWebHelper.cs b/Duplicati/Library/Backend/OAuthHelper/JSONWebHelper.cs index 0fa93b110..166a1ac56 100644 --- a/Duplicati/Library/Backend/OAuthHelper/JSONWebHelper.cs +++ b/Duplicati/Library/Backend/OAuthHelper/JSONWebHelper.cs @@ -1,4 +1,4 @@ -// Copyright (C) 2015, The Duplicati Team +// 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 @@ -19,7 +19,7 @@ using System; using System.Net; using Duplicati.Library.Utility; using Newtonsof namespace Duplicati.Library { public class JSONWebHelper - { public static readonly string USER_AGENT = string.Format("Duplicati v{0}", System.Reflection.Assembly.GetExecutingAssembly().GetName().Version.ToString()); private readonly string m_user_agent; public string OAuthLoginUrl { get; private set; } public string UserAgent { get { return m_user_agent; } } public event Action CreateSetupHelper; public JSONWebHelper(string useragent = null) { m_user_agent = useragent ?? USER_AGENT; } + { public static readonly string USER_AGENT = string.Format("Duplicati v{0}", System.Reflection.Assembly.GetExecutingAssembly().GetName().Version.ToString()); private readonly string m_user_agent; public string OAuthLoginUrl { get; private set; } public string UserAgent { get { return m_user_agent; } } public event Action CreateSetupHelper; public JSONWebHelper(string useragent = null) { m_user_agent = useragent ?? USER_AGENT; } public virtual HttpWebRequest CreateRequest(string url, string method = null) { var req = (HttpWebRequest)WebRequest.Create(url); req.UserAgent = UserAgent; if (method != null) req.Method = method; if (CreateSetupHelper != null) CreateSetupHelper(req); return req; } public T ReadJSONResponse(string url, object requestdata = null, string method = null) { if (requestdata is string) throw new ArgumentException("Cannot send string object as data"); if (method == null && requestdata != null) method = "POST"; return ReadJSONResponse(CreateRequest(url, method), requestdata); } public T ReadJSONResponse(HttpWebRequest req, object requestdata = null) { return ReadJSONResponse(new AsyncHttpRequest(req), requestdata); } public T ReadJSONResponse(AsyncHttpRequest req, object requestdata = null) { using(var resp = GetResponse(req, requestdata)) return ReadJSONResponse(resp); } public T ReadJSONResponse(HttpWebResponse resp) { using(var rs = resp.GetResponseStream()) using(var tr = new System.IO.StreamReader(rs)) using(var jr = new Newtonsoft.Json.JsonTextReader(tr)) return new Newtonsoft.Json.JsonSerializer().Deserialize(jr); } public HttpWebResponse GetResponseWithoutException(string url, string method = null) { return GetResponseWithoutException(CreateRequest(url, method)); } public HttpWebResponse GetResponseWithoutException(HttpWebRequest req) { return GetResponseWithoutException(new AsyncHttpRequest(req)); } public HttpWebResponse GetResponseWithoutException(AsyncHttpRequest req) { try { return (HttpWebResponse)req.GetResponse(); } catch(WebException wex) { if (wex.Response is HttpWebResponse) return (HttpWebResponse)wex.Response; throw; } } public HttpWebResponse GetResponse(string url, object requestdata = null, string method = null) { if (requestdata is string) throw new ArgumentException("Cannot send string object as data"); if (method == null && requestdata != null) method = "POST"; return GetResponse(CreateRequest(url, method), requestdata); } public HttpWebResponse GetResponse(HttpWebRequest req, object requestdata = null) { return GetResponse(new AsyncHttpRequest(req), requestdata); } public HttpWebResponse GetResponse(AsyncHttpRequest req, object requestdata = null) { if (requestdata != null) { if (requestdata is System.IO.Stream) { var stream = requestdata as System.IO.Stream; req.Request.ContentLength = stream.Length; if (string.IsNullOrEmpty(req.Request.ContentType)) req.Request.ContentType = "application/octet-stream"; using(var rs = req.GetRequestStream()) Library.Utility.Utility.CopyStream(stream, rs); } else { var data = Encoding.UTF8.GetBytes(JsonConvert.SerializeObject(requestdata)); req.Request.ContentLength = data.Length; req.Request.ContentType = "application/json; charset=UTF-8"; using(var rs = req.GetRequestStream()) rs.Write(data, 0, data.Length); } } return (HttpWebResponse)req.GetResponse(); } } } diff --git a/Duplicati/Library/Backend/OAuthHelper/OAuthHelper.cs b/Duplicati/Library/Backend/OAuthHelper/OAuthHelper.cs index 1f26c9ad1..8c6b44c91 100644 --- a/Duplicati/Library/Backend/OAuthHelper/OAuthHelper.cs +++ b/Duplicati/Library/Backend/OAuthHelper/OAuthHelper.cs @@ -1,4 +1,4 @@ -// Copyright (C) 2015, The Duplicati Team +// 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 @@ -14,7 +14,7 @@ // 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.Net; using Duplicati.Library.Utility; using System.Collections.Generic; using System.Web; +using System; using System.Net; using Duplicati.Library.Utility; using System.Collections.Generic; using System.Web; namespace Duplicati.Library { public class OAuthHelper : JSONWebHelper { private string m_token; private string m_authid; private string m_servicename; private DateTime m_tokenExpires = DateTime.UtcNow; private static string _override_server = null; public static string OAUTH_SERVER { get { return string.IsNullOrWhiteSpace(_override_server) ? DUPLICATI_OAUTH_SERVICE : _override_server; } set { _override_server = value; } } public const string DUPLICATI_OAUTH_SERVICE = "https://duplicati-oauth-handler.appspot.com/refresh"; private const string OAUTH_LOGIN_URL_TEMPLATE = "https://duplicati-oauth-handler.appspot.com/?type={0}"; public static string OAUTH_LOGIN_URL(string modulename) { return string.Format(OAUTH_LOGIN_URL_TEMPLATE, modulename); } public string OAuthLoginUrl { get; private set; } public bool AutoAuthHeader { get; set; } public OAuthHelper(string authid, string servicename, string useragent = null) : base(useragent) { m_authid = authid; OAuthLoginUrl = OAUTH_LOGIN_URL(servicename); if (string.IsNullOrEmpty(authid)) throw new Exception(Strings.OAuthHelper.MissingAuthID(OAuthLoginUrl)); } public T GetJSONData(string url, Action setup = null, Action setupreq = null) { var req = CreateRequest(url); if (setup != null) setup(req); var areq = new AsyncHttpRequest(req); if (setupreq != null) setupreq(areq); return ReadJSONResponse(areq); } public T GetTokenResponse() { var req = CreateRequest(OAUTH_SERVER); req.Headers["X-AuthID"] = m_authid; req.Timeout = (int)TimeSpan.FromSeconds(25).TotalMilliseconds; return ReadJSONResponse(req); } public override HttpWebRequest CreateRequest(string url, string method = null) { var r = base.CreateRequest(url, method); if (AutoAuthHeader && !OAUTH_SERVER.Equals(url)) r.Headers["Authorization"] = string.Format("Bearer {0}", AccessToken); return r; } public string AccessToken { get { if (m_token == null || m_tokenExpires < DateTime.UtcNow) { var retries = 0; while(true) { try { var res = GetTokenResponse(); m_tokenExpires = DateTime.UtcNow.AddSeconds(res.expires - 30); return m_token = res.access_token; } catch (Exception ex) { var msg = ex.Message; if (ex is WebException) { var resp = ((WebException)ex).Response as HttpWebResponse; if (resp != null) { msg = resp.Headers["X-Reason"]; if (string.IsNullOrWhiteSpace(msg)) msg = resp.StatusDescription; if (resp.StatusCode == HttpStatusCode.ServiceUnavailable) { if (msg == resp.StatusDescription) throw new Exception(Strings.OAuthHelper.OverQuotaError); else throw new Exception(Strings.OAuthHelper.AuthorizationFailure(msg, OAuthLoginUrl), ex); } } } if (retries >= 5) throw new Exception(Strings.OAuthHelper.AuthorizationFailure(msg, OAuthLoginUrl), ex); System.Threading.Thread.Sleep(TimeSpan.FromSeconds(Math.Pow(2, retries))); retries++; } } } return m_token; } } private class OAuth_Service_Response { public string access_token { get; set; } [Newtonsoft.Json.JsonProperty(NullValueHandling = Newtonsoft.Json.NullValueHandling.Ignore)] public int expires { get; set; } } } diff --git a/Duplicati/Library/Backend/OAuthHelper/Properties/AssemblyInfo.cs b/Duplicati/Library/Backend/OAuthHelper/Properties/AssemblyInfo.cs index e6477c4db..b82f1cbf4 100644 --- a/Duplicati/Library/Backend/OAuthHelper/Properties/AssemblyInfo.cs +++ b/Duplicati/Library/Backend/OAuthHelper/Properties/AssemblyInfo.cs @@ -1,4 +1,4 @@ -// Copyright (C) 2015, The Duplicati Team +// 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 diff --git a/Duplicati/Library/Backend/OpenStack/OpenStackConfig.cs b/Duplicati/Library/Backend/OpenStack/OpenStackConfig.cs index 85bac1916..c026f36a2 100644 --- a/Duplicati/Library/Backend/OpenStack/OpenStackConfig.cs +++ b/Duplicati/Library/Backend/OpenStack/OpenStackConfig.cs @@ -1,4 +1,4 @@ -// Copyright (C) 2015, The Duplicati Team +// 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 @@ -19,7 +19,7 @@ using System; using Duplicati.Library.Interface; using System.Collections.Generi namespace Duplicati.Library.Backend.OpenStack { public class SwiftConfig : IWebModule - { private const ConfigType DEFAULT_CONFIG_TYPE = ConfigType.Providers; private static readonly string DEFAULT_CONFIG_TYPE_STR = Enum.GetName(typeof(ConfigType), DEFAULT_CONFIG_TYPE); private const string KEY_CONFIGTYPE = "openstack-config"; public enum ConfigType { Providers, } + { private const ConfigType DEFAULT_CONFIG_TYPE = ConfigType.Providers; private static readonly string DEFAULT_CONFIG_TYPE_STR = Enum.GetName(typeof(ConfigType), DEFAULT_CONFIG_TYPE); private const string KEY_CONFIGTYPE = "openstack-config"; public enum ConfigType { Providers, } public SwiftConfig() { } public System.Collections.Generic.IDictionary Execute(System.Collections.Generic.IDictionary options) { string k; options.TryGetValue(KEY_CONFIGTYPE, out k); if (string.IsNullOrWhiteSpace(k)) k = DEFAULT_CONFIG_TYPE_STR; ConfigType ct; if (!Enum.TryParse(k, true, out ct)) ct = DEFAULT_CONFIG_TYPE; switch (ct) { default: return OpenStack.OpenStackStorage.KNOWN_OPENSTACK_PROVIDERS.ToDictionary((x) => x.Key, (y) => y.Value); } } public string Key { get { return "openstack-getconfig"; } } public string DisplayName { get { return "OpenStack configuration module"; } } public string Description { get { return "Exposes OpenStack configuration as a web module"; } } public System.Collections.Generic.IList SupportedCommands { get { return new List(new ICommandLineArgument[] { new CommandLineArgument(KEY_CONFIGTYPE, CommandLineArgument.ArgumentType.Enumeration, "The config to get", "Provides different config values", DEFAULT_CONFIG_TYPE_STR, Enum.GetNames(typeof(ConfigType))) }); } } diff --git a/Duplicati/Library/Backend/OpenStack/OpenStackStorage.cs b/Duplicati/Library/Backend/OpenStack/OpenStackStorage.cs index f2d5eb947..8790348ff 100644 --- a/Duplicati/Library/Backend/OpenStack/OpenStackStorage.cs +++ b/Duplicati/Library/Backend/OpenStack/OpenStackStorage.cs @@ -1,4 +1,4 @@ -// Copyright (C) 2015, The Duplicati Team +// 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 diff --git a/Duplicati/Library/Backend/OpenStack/Properties/AssemblyInfo.cs b/Duplicati/Library/Backend/OpenStack/Properties/AssemblyInfo.cs index 97ee4af0b..a83cdd27d 100644 --- a/Duplicati/Library/Backend/OpenStack/Properties/AssemblyInfo.cs +++ b/Duplicati/Library/Backend/OpenStack/Properties/AssemblyInfo.cs @@ -1,4 +1,4 @@ -// Copyright (C) 2015, The Duplicati Team +// 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 diff --git a/Duplicati/Library/Backend/OpenStack/Strings.cs b/Duplicati/Library/Backend/OpenStack/Strings.cs index c8eb8fb01..74faa2ad5 100644 --- a/Duplicati/Library/Backend/OpenStack/Strings.cs +++ b/Duplicati/Library/Backend/OpenStack/Strings.cs @@ -1,4 +1,4 @@ -// Copyright (C) 2015, The Duplicati Team +// 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 diff --git a/Duplicati/Library/Main/Operation/RestoreHandlerMetadataStorage.cs b/Duplicati/Library/Main/Operation/RestoreHandlerMetadataStorage.cs index e52b0a1ba..39b663d17 100644 --- a/Duplicati/Library/Main/Operation/RestoreHandlerMetadataStorage.cs +++ b/Duplicati/Library/Main/Operation/RestoreHandlerMetadataStorage.cs @@ -1,4 +1,4 @@ -// Copyright (C) 2015, The Duplicati Team +// 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 @@ -14,11 +14,11 @@ // 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.IO; using Duplicati.Library.Utility; using System.Collections.Generic; +using System; using System.IO; using Duplicati.Library.Utility; using System.Collections.Generic; namespace Duplicati.Library.Main.Operation { public class RestoreHandlerMetadataStorage : IDisposable - { private TempFile m_temp; private FileStream m_stream; private long m_entries; private long m_filepos; + { private TempFile m_temp; private FileStream m_stream; private long m_entries; private long m_filepos; public RestoreHandlerMetadataStorage() { m_temp = new TempFile(); m_stream = File.Open(m_temp, FileMode.Truncate, FileAccess.ReadWrite, FileShare.None); } public void Add(string path, Stream data) { var datalen = data.Length; if (datalen > Int32.MaxValue) throw new ArgumentOutOfRangeException("Metadata is larger than int32"); var pathbytes = System.Text.Encoding.UTF8.GetBytes(path); var pathlen = BitConverter.GetBytes(pathbytes.LongLength); var entrylen = BitConverter.GetBytes(datalen); var totalsize = pathbytes.Length + pathlen.Length + entrylen.Length + datalen; m_stream.Position = m_filepos; m_stream.Write(pathlen, 0, pathlen.Length); m_stream.Write(pathbytes, 0, pathbytes.Length); m_stream.Write(entrylen, 0, entrylen.Length); data.CopyTo(m_stream); if (m_stream.Position != m_filepos + totalsize) throw new Exception("Bad file write!"); m_filepos += totalsize; m_entries++; } private void CheckedRead(byte[] buffer, int offset, int count) { int r; while (count > 0 && (r = m_stream.Read(buffer, offset, count)) > 0) { offset += r; count -= r; } if (count != 0) throw new Exception("Bad file read"); } public IEnumerable> Records { get { long pos = 0; var bf = BitConverter.GetBytes(0L); var buf = new byte[8 * 1024]; Logging.Log.WriteMessage(string.Format("The metadata storage file has {0} entries and takes up {1}", m_entries, Library.Utility.Utility.FormatSizeString(m_stream.Length)), Duplicati.Library.Logging.LogMessageType.Profiling); using(new Logging.Timer("Read metadata from file")) for(var e = 0L; e < m_entries; e++) { m_stream.Position = pos; CheckedRead(bf, 0, bf.Length); var stringlen = BitConverter.ToInt64(bf, 0); var strbuf = stringlen > buf.Length ? new byte[stringlen] : buf; CheckedRead(strbuf, 0, (int)stringlen); var path = System.Text.Encoding.UTF8.GetString(strbuf, 0, (int)stringlen); CheckedRead(bf, 0, bf.Length); var datalen = BitConverter.ToInt64(bf, 0); if (datalen > Int32.MaxValue) throw new ArgumentOutOfRangeException("Metadata is larger than int32"); var databuf = datalen > buf.Length ? new byte[datalen] : buf; CheckedRead(databuf, 0, (int)datalen); pos += datalen + stringlen + bf.Length + bf.Length; yield return new KeyValuePair(path, new MemoryStream(databuf, 0, (int)datalen)); } } } diff --git a/Duplicati/Server/WebServer/RESTHandler.cs b/Duplicati/Server/WebServer/RESTHandler.cs index aaa86424d..6b240bc31 100644 --- a/Duplicati/Server/WebServer/RESTHandler.cs +++ b/Duplicati/Server/WebServer/RESTHandler.cs @@ -1,4 +1,4 @@ -// Copyright (C) 2015, The Duplicati Team +// 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 @@ -14,11 +14,174 @@ // 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.HttpModules; using System.Collections.Generic; using Duplicati.Server.WebServer.RESTMethods; +using System; +using System.Linq; +using HttpServer.HttpModules; +using System.Collections.Generic; +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.InvariantCultureIgnoreCase); 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.DefinedTypes 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, BodyWriter bw, 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(), (request.Method.ToUpper() == "POST" ? request.Form : request.QueryString)["id"].Value); } public static void DoProcess(RequestInfo info, string method, string module, string key) { try { 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) { 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); } ((IRESTMethodGET)mod).GET(key, info); } else if (method == HttpServer.Method.Put && mod is IRESTMethodPUT) ((IRESTMethodPUT)mod).PUT(key, info); else if (method == HttpServer.Method.Post && mod is IRESTMethodPOST) { if (info.Request.Form == HttpServer.HttpForm.EmptyForm) { 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); } ((IRESTMethodPOST)mod).POST(key, info); } else if (method == HttpServer.Method.Delete && mod is IRESTMethodDELETE) ((IRESTMethodDELETE)mod).DELETE(key, info); else if (method == "PATCH" && mod is IRESTMethodPATCH) ((IRESTMethodPATCH)mod).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.ToString()); 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 = ex.Message, Type = ex.GetType().Name, #if DEBUG Stacktrace = ex.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.InvariantCultureIgnoreCase)) 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; } } + { + 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.InvariantCultureIgnoreCase); + + 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.DefinedTypes + 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, BodyWriter bw, 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(), (request.Method.ToUpper() == "POST" ? request.Form : request.QueryString)["id"].Value); + } + + public static void DoProcess(RequestInfo info, string method, string module, string key) + { + try + { + 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) + { + 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); + } + ((IRESTMethodGET)mod).GET(key, info); + } + else if (method == HttpServer.Method.Put && mod is IRESTMethodPUT) + ((IRESTMethodPUT)mod).PUT(key, info); + else if (method == HttpServer.Method.Post && mod is IRESTMethodPOST) + { + if (info.Request.Form == HttpServer.HttpForm.EmptyForm) + { + 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); + } + ((IRESTMethodPOST)mod).POST(key, info); + } + else if (method == HttpServer.Method.Delete && mod is IRESTMethodDELETE) + ((IRESTMethodDELETE)mod).DELETE(key, info); + else if (method == "PATCH" && mod is IRESTMethodPATCH) + ((IRESTMethodPATCH)mod).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.ToString()); + + 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 = ex.Message, + Type = ex.GetType().Name, + #if DEBUG + Stacktrace = ex.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.InvariantCultureIgnoreCase)) + 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/RESTMethods/Acknowledgements.cs b/Duplicati/Server/WebServer/RESTMethods/Acknowledgements.cs index dbf437eb7..27561f530 100644 --- a/Duplicati/Server/WebServer/RESTMethods/Acknowledgements.cs +++ b/Duplicati/Server/WebServer/RESTMethods/Acknowledgements.cs @@ -1,4 +1,4 @@ -// Copyright (C) 2015, The Duplicati Team +// 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 @@ -14,12 +14,38 @@ // 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; +using System.Collections.Generic; 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 = System.IO.Path.Combine(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)), }; } } } + { + private class GetResponse + { + public string Status; + public string Acknowledgements; + } + + public void GET(string key, RequestInfo info) + { + var path = System.IO.Path.Combine(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/Server/WebServer/RESTMethods/Backup.cs index c92a37e44..36130559d 100644 --- a/Duplicati/Server/WebServer/RESTMethods/Backup.cs +++ b/Duplicati/Server/WebServer/RESTMethods/Backup.cs @@ -1,4 +1,4 @@ -// Copyright (C) 2015, The Duplicati Team +// 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 @@ -14,12 +14,573 @@ // 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 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 ?? "").Split(new string[] { System.IO.Path.PathSeparator.ToString() }, StringSplitOptions.RemoveEmptyEntries); 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"); 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, 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; info.OutputOK(result); } private void ListFileSets(IBackup backup, RequestInfo info) { var input = info.Request.QueryString; var extra = new Dictionary(); extra["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; info.OutputOK(r.Filesets); } private void FetchLogData(IBackup backup, RequestInfo info) { using(var con = (System.Data.IDbConnection)Activator.CreateInstance(Duplicati.Library.SQLiteHelper.SQLiteLoader.SQLiteConnectionType)) { con.ConnectionString = "Data Source=" + backup.DBPath; con.Open(); 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 = (System.Data.IDbConnection)Activator.CreateInstance(Duplicati.Library.SQLiteHelper.SQLiteLoader.SQLiteConnectionType)) { con.ConnectionString = "Data Source=" + backup.DBPath; con.Open(); 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 Export(IBackup backup, RequestInfo info) { var cmdline = Library.Utility.Utility.ParseBool(info.Request.QueryString["cmdline"].Value, false); if (cmdline) { info.OutputOK(new { Command = Runner.GetCommandLine(Runner.CreateTask(DuplicatiOperation.Backup, backup)) }); } else { var passphrase = info.Request.QueryString["passphrase"].Value; var ipx = Program.DataConnection.PrepareBackupForExport(backup); byte[] data; using(var ms = new System.IO.MemoryStream()) using(var sw = new System.IO.StreamWriter(ms)) { Serializer.SerializeJson(sw, ipx, true); if (!string.IsNullOrWhiteSpace(passphrase)) { ms.Position = 0; using(var ms2 = new System.IO.MemoryStream()) using(var m = new Duplicati.Library.Encryption.AESEncryption(passphrase, new Dictionary())) { m.Encrypt(ms, ms2); data = ms2.ToArray(); } } else data = ms.ToArray(); } var 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); } } private void RestoreFiles(IBackup backup, RequestInfo info) { var input = info.Request.Form; var filters = input["paths"].Value.Split(new string[] { System.IO.Path.PathSeparator.ToString() }, StringSplitOptions.RemoveEmptyEntries); 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); 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 Repair(IBackup backup, RequestInfo info) { DoRepair(backup, info, false); } private void RepairUpdate(IBackup backup, RequestInfo info) { DoRepair(backup, info, true); } 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 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 = input["paths"].Value.Split(new string[] { System.IO.Path.PathSeparator.ToString() }, StringSplitOptions.RemoveEmptyEntries); 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.Where(x => { var bn = x == null ? null : x.Backup; return bn == null || bn.ID == backup.ID; }).Any()) { // Already in queue } else { Program.WorkThread.AddTask(Runner.CreateTask(DuplicatiOperation.Backup, backup)); Program.StatusEventNotifyer.SignalNewEvent(); } info.OutputOK(); } private void IsActive(IBackup backup, RequestInfo info) { var t = Program.WorkThread.CurrentTask; var bt = t == null ? null : t.Backup; if (bt != null && backup.ID == bt.ID) { info.OutputOK(new { Status = "OK", Active = true }); return; } else if (Program.WorkThread.CurrentTasks.Where(x => { var bn = x == null ? null : x.Backup; return bn == null || bn.ID == backup.ID; }).Any()) { info.OutputOK(new { Status = "OK", Active = true }); return; } else { info.OutputOK(new { Status = "OK", Active = false }); return; } } 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"); else { if (parts.Length > 1) { var operation = parts.Last().Split(new char[] {'/'}).First().ToLowerInvariant(); switch (operation) { case "files": SearchFiles(bk, parts.Last().Split(new char[] { '/' }, 2).LastOrDefault(), 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 "isactive": IsActive(bk, info); return; default: info.ReportClientError(string.Format("Invalid component: {0}", operation)); 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"); 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 "restore": RestoreFiles(bk, info); return; case "createreport": CreateReport(bk, info); return; case "repair": Repair(bk, info); return; case "repairupdate": RepairUpdate(bk, info); return; case "verify": Verify(bk, info); return; case "start": case "run": RunBackup(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"); } } public void PUT(string key, RequestInfo info) { string str = info.Request.Form["data"].Value; if (string.IsNullOrWhiteSpace(str)) { info.ReportClientError("Missing backup object"); return; } Backups.AddOrUpdateBackupData data = null; try { data = Serializer.Deserialize(new StringReader(str)); if (data.Backup == null) { info.ReportClientError("Data object had no backup entry"); return; } if (!string.IsNullOrEmpty(key)) data.Backup.ID = key; if (string.IsNullOrEmpty(data.Backup.ID)) { info.ReportClientError("Invalid or missing backup id"); 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"); return; } if (Program.DataConnection.Backups.Where(x => x.Name.Equals(data.Backup.Name, StringComparison.InvariantCultureIgnoreCase) && x.ID != data.Backup.ID).Any()) { info.ReportClientError("There already exists a backup with the name: " + data.Backup.Name); 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)); else info.ReportClientError(string.Format("Unable to save backup or schedule: {0}", ex.Message)); } } public void DELETE(string key, RequestInfo info) { var backup = Program.DataConnection.GetBackup(key); if (backup == null) { info.ReportClientError("Invalid or missing backup id"); 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(); try { 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; } finally { } 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 dbpath = backup.DBPath; Program.DataConnection.DeleteBackup(backup); // TODO: Before we activate this, // we need some strategy to figure out // if the db is shared with something else // like the commandline or another backup /*try { if (System.IO.File.Exists(dbpath)) System.IO.File.Delete(dbpath); } catch (Exception ex) { Program.DataConnection.LogError(null, string.Format("Failed to delete database: {0}", dbpath), ex); }*/ //We have fiddled with the schedules Program.Scheduler.Reschedule(); info.OutputOK(); } - 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)) }; } } } + { + 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 ?? "").Split(new string[] { System.IO.Path.PathSeparator.ToString() }, StringSplitOptions.RemoveEmptyEntries); + 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"); + 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, 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; + + info.OutputOK(result); + + } + + private void ListFileSets(IBackup backup, RequestInfo info) + { + var input = info.Request.QueryString; + var extra = new Dictionary(); + extra["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; + + info.OutputOK(r.Filesets); + } + + private void FetchLogData(IBackup backup, RequestInfo info) + { + using(var con = (System.Data.IDbConnection)Activator.CreateInstance(Duplicati.Library.SQLiteHelper.SQLiteLoader.SQLiteConnectionType)) + { + con.ConnectionString = "Data Source=" + backup.DBPath; + con.Open(); + + 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 = (System.Data.IDbConnection)Activator.CreateInstance(Duplicati.Library.SQLiteHelper.SQLiteLoader.SQLiteConnectionType)) + { + con.ConnectionString = "Data Source=" + backup.DBPath; + con.Open(); + + 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 Export(IBackup backup, RequestInfo info) + { + var cmdline = Library.Utility.Utility.ParseBool(info.Request.QueryString["cmdline"].Value, false); + if (cmdline) + { + info.OutputOK(new { Command = Runner.GetCommandLine(Runner.CreateTask(DuplicatiOperation.Backup, backup)) }); + } + else + { + var passphrase = info.Request.QueryString["passphrase"].Value; + var ipx = Program.DataConnection.PrepareBackupForExport(backup); + + byte[] data; + using(var ms = new System.IO.MemoryStream()) + using(var sw = new System.IO.StreamWriter(ms)) + { + Serializer.SerializeJson(sw, ipx, true); + + if (!string.IsNullOrWhiteSpace(passphrase)) + { + ms.Position = 0; + using(var ms2 = new System.IO.MemoryStream()) + using(var m = new Duplicati.Library.Encryption.AESEncryption(passphrase, new Dictionary())) + { + m.Encrypt(ms, ms2); + data = ms2.ToArray(); + } + } + else + data = ms.ToArray(); + } + + var 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); + } + } + + private void RestoreFiles(IBackup backup, RequestInfo info) + { + var input = info.Request.Form; + + var filters = input["paths"].Value.Split(new string[] { System.IO.Path.PathSeparator.ToString() }, StringSplitOptions.RemoveEmptyEntries); + 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); + + 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 Repair(IBackup backup, RequestInfo info) + { + DoRepair(backup, info, false); + } + + private void RepairUpdate(IBackup backup, RequestInfo info) + { + DoRepair(backup, info, true); + } + + 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 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 = input["paths"].Value.Split(new string[] { System.IO.Path.PathSeparator.ToString() }, StringSplitOptions.RemoveEmptyEntries); + + 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.Where(x => { + var bn = x == null ? null : x.Backup; + return bn == null || bn.ID == backup.ID; + }).Any()) + { + // Already in queue + } + else + { + Program.WorkThread.AddTask(Runner.CreateTask(DuplicatiOperation.Backup, backup)); + Program.StatusEventNotifyer.SignalNewEvent(); + } + + info.OutputOK(); + } + + private void IsActive(IBackup backup, RequestInfo info) + { + var t = Program.WorkThread.CurrentTask; + var bt = t == null ? null : t.Backup; + if (bt != null && backup.ID == bt.ID) + { + info.OutputOK(new { Status = "OK", Active = true }); + return; + } + else if (Program.WorkThread.CurrentTasks.Where(x => + { + var bn = x == null ? null : x.Backup; + return bn == null || bn.ID == backup.ID; + }).Any()) + { + info.OutputOK(new { Status = "OK", Active = true }); + return; + } + else + { + info.OutputOK(new { Status = "OK", Active = false }); + return; + } + } + + 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"); + else + { + if (parts.Length > 1) + { + var operation = parts.Last().Split(new char[] {'/'}).First().ToLowerInvariant(); + + switch (operation) + { + case "files": + SearchFiles(bk, parts.Last().Split(new char[] { '/' }, 2).LastOrDefault(), 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 "isactive": + IsActive(bk, info); + return; + default: + info.ReportClientError(string.Format("Invalid component: {0}", operation)); + 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"); + 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 "restore": + RestoreFiles(bk, info); + return; + + case "createreport": + CreateReport(bk, info); + return; + + case "repair": + Repair(bk, info); + return; + + case "repairupdate": + RepairUpdate(bk, info); + return; + + case "verify": + Verify(bk, info); + return; + + case "start": + case "run": + RunBackup(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"); + } + } + + public void PUT(string key, RequestInfo info) + { + string str = info.Request.Form["data"].Value; + if (string.IsNullOrWhiteSpace(str)) + { + info.ReportClientError("Missing backup object"); + return; + } + + Backups.AddOrUpdateBackupData data = null; + try + { + data = Serializer.Deserialize(new StringReader(str)); + if (data.Backup == null) + { + info.ReportClientError("Data object had no backup entry"); + return; + } + + if (!string.IsNullOrEmpty(key)) + data.Backup.ID = key; + + if (string.IsNullOrEmpty(data.Backup.ID)) + { + info.ReportClientError("Invalid or missing backup id"); + 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"); + return; + } + + if (Program.DataConnection.Backups.Where(x => x.Name.Equals(data.Backup.Name, StringComparison.InvariantCultureIgnoreCase) && x.ID != data.Backup.ID).Any()) + { + info.ReportClientError("There already exists a backup with the name: " + data.Backup.Name); + 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)); + else + info.ReportClientError(string.Format("Unable to save backup or schedule: {0}", ex.Message)); + + } + } + + public void DELETE(string key, RequestInfo info) + { + var backup = Program.DataConnection.GetBackup(key); + if (backup == null) + { + info.ReportClientError("Invalid or missing backup id"); + 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(); + + try + { + 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; + } + finally + { + } + + 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 dbpath = backup.DBPath; + Program.DataConnection.DeleteBackup(backup); + + // TODO: Before we activate this, + // we need some strategy to figure out + // if the db is shared with something else + // like the commandline or another backup + /*try + { + if (System.IO.File.Exists(dbpath)) + System.IO.File.Delete(dbpath); + } + catch (Exception ex) + { + Program.DataConnection.LogError(null, string.Format("Failed to delete database: {0}", dbpath), ex); + }*/ + + //We have fiddled with the schedules + Program.Scheduler.Reschedule(); + + info.OutputOK(); + } + 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/Server/WebServer/RESTMethods/BackupDefaults.cs index a6841c568..2b50d79d0 100644 --- a/Duplicati/Server/WebServer/RESTMethods/BackupDefaults.cs +++ b/Duplicati/Server/WebServer/RESTMethods/BackupDefaults.cs @@ -1,4 +1,4 @@ -// Copyright (C) 2015, The Duplicati Team +// 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 @@ -14,14 +14,116 @@ // 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; +using System.Linq; namespace Duplicati.Server.WebServer.RESTMethods { public class BackupDefaults : IRESTMethodGET { - 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(System.Reflection.Assembly.GetExecutingAssembly().GetName().Name + ".newbackup.json"))) n = (Newtonsoft.Json.Linq.JObject)Newtonsoft.Json.JsonConvert.DeserializeObject(s.ReadToEnd()); MergeJsonObjects(o, n); } catch { } try { // Add install defaults/overrides, if present var path = System.IO.Path.Combine(Duplicati.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 { } 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; } } } } + 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(System.Reflection.Assembly.GetExecutingAssembly().GetName().Name + ".newbackup.json"))) + n = (Newtonsoft.Json.Linq.JObject)Newtonsoft.Json.JsonConvert.DeserializeObject(s.ReadToEnd()); + + MergeJsonObjects(o, n); + } + catch + { + } + + try + { + // Add install defaults/overrides, if present + var path = System.IO.Path.Combine(Duplicati.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 + { + } + + 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/Server/WebServer/RESTMethods/Backups.cs index 354119c2a..33251a881 100644 --- a/Duplicati/Server/WebServer/RESTMethods/Backups.cs +++ b/Duplicati/Server/WebServer/RESTMethods/Backups.cs @@ -1,4 +1,4 @@ -// Copyright (C) 2015, The Duplicati Team +// 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 @@ -14,12 +14,194 @@ // 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 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 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() { 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); 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 { Serializable.ImportExportStructure ipx; var file = info.Request.Form.GetFile("config"); if (file == null) throw new Exception("No file uploaded"); var buf = new byte[3]; using(var fs = System.IO.File.OpenRead(file.Filename)) { fs.Read(buf, 0, buf.Length); fs.Position = 0; if (buf[0] == 'A' && buf[1] == 'E' && buf[2] == 'S') { var passphrase = input["passphrase"].Value; using(var m = new Duplicati.Library.Encryption.AESEncryption(passphrase, 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); } } ipx.Backup.ID = null; ((Database.Backup)ipx.Backup).DBPath = null; if (ipx.Schedule != null) ipx.Schedule.ID = -1; lock(Program.DataConnection.m_lock) { var basename = ipx.Backup.Name; var c = 0; while (c++ < 100 && Program.DataConnection.Backups.Where(x => x.Name.Equals(ipx.Backup.Name, StringComparison.InvariantCultureIgnoreCase)).Any()) ipx.Backup.Name = basename + " (" + c.ToString() + ")"; if (Program.DataConnection.Backups.Where(x => x.Name.Equals(ipx.Backup.Name, StringComparison.InvariantCultureIgnoreCase)).Any()) { 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("\'", "\\'"))); } Program.DataConnection.AddOrUpdateBackupAndSchedule(ipx.Backup, ipx.Schedule); } info.Response.ContentType = "text/html"; info.BodyWriter.Write(output_template.Replace("MSG", "OK")); //bw.OutputOK(new { status = "OK", ID = ipx.Backup.ID }); } } 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 void POST(string key, RequestInfo info) { if ("import".Equals(key, StringComparison.InvariantCultureIgnoreCase)) { ImportBackup(info); return; } AddOrUpdateBackupData data = null; try { data = Serializer.Deserialize(new StringReader(info.Request.Form["data"].Value)); if (data.Backup == null) { info.ReportClientError("Data object had no backup entry"); 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; 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.Where(x => x.Name.Equals(data.Backup.Name, StringComparison.InvariantCultureIgnoreCase)).Any()) { info.ReportClientError("There already exists a backup with the name: " + data.Backup.Name); 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)); else info.ReportClientError(string.Format("Unable to save schedule or backup object: {0}", ex.Message)); } } - 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)) }; } } } + { + public class AddOrUpdateBackupData + { + 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() { + 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); + 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 + { + Serializable.ImportExportStructure ipx; + + var file = info.Request.Form.GetFile("config"); + if (file == null) + throw new Exception("No file uploaded"); + + var buf = new byte[3]; + using(var fs = System.IO.File.OpenRead(file.Filename)) + { + fs.Read(buf, 0, buf.Length); + + fs.Position = 0; + if (buf[0] == 'A' && buf[1] == 'E' && buf[2] == 'S') + { + var passphrase = input["passphrase"].Value; + using(var m = new Duplicati.Library.Encryption.AESEncryption(passphrase, 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); + } + } + + ipx.Backup.ID = null; + ((Database.Backup)ipx.Backup).DBPath = null; + + if (ipx.Schedule != null) + ipx.Schedule.ID = -1; + + lock(Program.DataConnection.m_lock) + { + var basename = ipx.Backup.Name; + var c = 0; + while (c++ < 100 && Program.DataConnection.Backups.Where(x => x.Name.Equals(ipx.Backup.Name, StringComparison.InvariantCultureIgnoreCase)).Any()) + ipx.Backup.Name = basename + " (" + c.ToString() + ")"; + + if (Program.DataConnection.Backups.Where(x => x.Name.Equals(ipx.Backup.Name, StringComparison.InvariantCultureIgnoreCase)).Any()) + { + 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("\'", "\\'"))); + } + + Program.DataConnection.AddOrUpdateBackupAndSchedule(ipx.Backup, ipx.Schedule); + } + + info.Response.ContentType = "text/html"; + info.BodyWriter.Write(output_template.Replace("MSG", "OK")); + + //bw.OutputOK(new { status = "OK", ID = ipx.Backup.ID }); + } + } + 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 void POST(string key, RequestInfo info) + { + if ("import".Equals(key, StringComparison.InvariantCultureIgnoreCase)) + { + ImportBackup(info); + return; + } + + AddOrUpdateBackupData data = null; + try + { + data = Serializer.Deserialize(new StringReader(info.Request.Form["data"].Value)); + if (data.Backup == null) + { + info.ReportClientError("Data object had no backup entry"); + 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; + + 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.Where(x => x.Name.Equals(data.Backup.Name, StringComparison.InvariantCultureIgnoreCase)).Any()) + { + info.ReportClientError("There already exists a backup with the name: " + data.Backup.Name); + 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)); + else + info.ReportClientError(string.Format("Unable to save schedule or backup object: {0}", ex.Message)); + } + } + + + 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/Server/WebServer/RESTMethods/BugReport.cs index 49b248ce6..99cf649fb 100644 --- a/Duplicati/Server/WebServer/RESTMethods/BugReport.cs +++ b/Duplicati/Server/WebServer/RESTMethods/BugReport.cs @@ -1,4 +1,4 @@ -// Copyright (C) 2015, The Duplicati Team +// 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 @@ -14,12 +14,44 @@ // 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; +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().Where(x => x.ID == id).FirstOrDefault(); if (tf == null) { info.ReportClientError("Invalid or missing backup id"); return; } if (!System.IO.File.Exists(tf.Path)) { info.ReportClientError("File is missing"); 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(); } } + { + public void GET(string key, RequestInfo info) + { + long id; + long.TryParse(key, out id); + + var tf = Program.DataConnection.GetTempFiles().Where(x => x.ID == id).FirstOrDefault(); + if (tf == null) + { + info.ReportClientError("Invalid or missing backup id"); + return; + } + + if (!System.IO.File.Exists(tf.Path)) + { + info.ReportClientError("File is missing"); + 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/Changelog.cs b/Duplicati/Server/WebServer/RESTMethods/Changelog.cs index 81194afa0..0fda33ebc 100644 --- a/Duplicati/Server/WebServer/RESTMethods/Changelog.cs +++ b/Duplicati/Server/WebServer/RESTMethods/Changelog.cs @@ -1,4 +1,4 @@ -// Copyright (C) 2015, The Duplicati Team +// 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 @@ -14,12 +14,61 @@ // 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; +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"); } 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)), }; } } + { + 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"); + } + 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/Filesystem.cs b/Duplicati/Server/WebServer/RESTMethods/Filesystem.cs index 9274a0a2a..a1a9adae5 100644 --- a/Duplicati/Server/WebServer/RESTMethods/Filesystem.cs +++ b/Duplicati/Server/WebServer/RESTMethods/Filesystem.cs @@ -1,4 +1,4 @@ -// Copyright (C) 2015, The Duplicati Team +// 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 @@ -19,7 +19,7 @@ using System; using System.Collections.Generic; using System.Linq; using System. namespace Duplicati.Server.WebServer.RESTMethods { public class Filesystem : IRESTMethodGET, 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)) { info.ReportClientError("No path parameter was found"); return; } bool skipFiles = Library.Utility.Utility.ParseBool(info.Request.QueryString["onlyfolders"].Value, false); string specialpath = null; string specialtoken = null; if (path.StartsWith("%")) { var ix = path.IndexOf("%", 1); if (ix > 0) { var tk = path.Substring(0, ix + 1); var node = SpecialFolders.Nodes.Where(x => x.id.Equals(tk, StringComparison.InvariantCultureIgnoreCase)).FirstOrDefault(); if (node != null) { specialpath = node.resolvedpath; specialtoken = node.id; } } } path = SpecialFolders.ExpandEnvironmentVariables(path); if (Duplicati.Library.Utility.Utility.IsClientLinux && !path.StartsWith("/")) { info.ReportClientError("The path parameter must start with a forward-slash"); return; } if (!string.IsNullOrWhiteSpace(command)) { if ("validate".Equals(command, StringComparison.InvariantCultureIgnoreCase)) { try { if (System.IO.Path.IsPathRooted(path) && System.IO.Directory.Exists(path)) { info.OutputOK(); return; } } catch { } info.ReportServerError("File or folder not found"); return; } else { info.ReportClientError(string.Format("No such operation found: {0}", command)); return; } } try { if (path != "" && path != "/") path = Duplicati.Library.Utility.Utility.AppendDirSeparator(path); IEnumerable res; if (!Library.Utility.Utility.IsClientLinux && (path.Equals("/") || path.Equals(""))) { res = from di in System.IO.DriveInfo.GetDrives() where di.DriveType == DriveType.Fixed || di.DriveType == DriveType.Network || di.DriveType == DriveType.Removable select new Serializable.TreeNode() { id = di.RootDirectory.FullName, text = di.RootDirectory.FullName.Replace('\\', ' ') + "(" + di.DriveType + ")", iconCls = "x-tree-icon-drive" }; } else { res = ListFolderAsNodes(path, skipFiles); } if ((path.Equals("/") || path.Equals("")) && specialtoken == null) { // Prepend special folders res = SpecialFolders.Nodes.Union(res); } if (specialtoken != null) { res = from n in res select new Serializable.TreeNode() { id = specialtoken + n.id.Substring(specialpath.Length), text = n.text, iconCls = n.iconCls, leaf = n.leaf, resolvedpath = n.id }; } info.OutputOK(res); } catch (Exception ex) { info.ReportClientError("Failed to process the path: " + ex.Message); } } private static IEnumerable ListFolderAsNodes(string entrypath, bool skipFiles) { //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; }; var systemIO = Library.Utility.Utility.IsClientLinux ? (Duplicati.Library.Snapshots.ISystemIO)new Duplicati.Library.Snapshots.SystemIOLinux() : (Duplicati.Library.Snapshots.ISystemIO)new Duplicati.Library.Snapshots.SystemIOWindows(); foreach (var s in System.IO.Directory.EnumerateFileSystemEntries(entrypath)) { Serializable.TreeNode tn = null; try { var attr = systemIO.GetFileAttributes(s); //var isSymlink = (attr & FileAttributes.ReparsePoint) != 0; var isFolder = (attr & FileAttributes.Directory) != 0; var isFile = !isFolder; //var isHidden = (attr & FileAttributes.Hidden) != 0; var accesible = isFile || canAccess(s); var isLeaf = isFile || !accesible || isEmptyFolder(s); var rawid = isFolder ? Library.Utility.Utility.AppendDirSeparator(s) : s; if (!skipFiles || isFolder) tn = new Serializable.TreeNode() { id = rawid, text = systemIO.PathGetFileName(s), iconCls = isFolder ? (accesible ? "x-tree-icon-parent" : "x-tree-icon-locked") : "x-tree-icon-leaf", leaf = isLeaf }; } catch { } if (tn != null) yield return tn; } } public string Description { get { return "Enumerates the server filesystem"; } } public IEnumerable> Types { get { return new KeyValuePair[] { new KeyValuePair(HttpServer.Method.Get, typeof(string[])), }; } } + { 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)) { info.ReportClientError("No path parameter was found"); return; } bool skipFiles = Library.Utility.Utility.ParseBool(info.Request.QueryString["onlyfolders"].Value, false); string specialpath = null; string specialtoken = null; if (path.StartsWith("%")) { var ix = path.IndexOf("%", 1); if (ix > 0) { var tk = path.Substring(0, ix + 1); var node = SpecialFolders.Nodes.Where(x => x.id.Equals(tk, StringComparison.InvariantCultureIgnoreCase)).FirstOrDefault(); if (node != null) { specialpath = node.resolvedpath; specialtoken = node.id; } } } path = SpecialFolders.ExpandEnvironmentVariables(path); if (Duplicati.Library.Utility.Utility.IsClientLinux && !path.StartsWith("/")) { info.ReportClientError("The path parameter must start with a forward-slash"); return; } if (!string.IsNullOrWhiteSpace(command)) { if ("validate".Equals(command, StringComparison.InvariantCultureIgnoreCase)) { try { if (System.IO.Path.IsPathRooted(path) && System.IO.Directory.Exists(path)) { info.OutputOK(); return; } } catch { } info.ReportServerError("File or folder not found"); return; } else { info.ReportClientError(string.Format("No such operation found: {0}", command)); return; } } try { if (path != "" && path != "/") path = Duplicati.Library.Utility.Utility.AppendDirSeparator(path); IEnumerable res; if (!Library.Utility.Utility.IsClientLinux && (path.Equals("/") || path.Equals(""))) { res = from di in System.IO.DriveInfo.GetDrives() where di.DriveType == DriveType.Fixed || di.DriveType == DriveType.Network || di.DriveType == DriveType.Removable select new Serializable.TreeNode() { id = di.RootDirectory.FullName, text = di.RootDirectory.FullName.Replace('\\', ' ') + "(" + di.DriveType + ")", iconCls = "x-tree-icon-drive" }; } else { res = ListFolderAsNodes(path, skipFiles); } if ((path.Equals("/") || path.Equals("")) && specialtoken == null) { // Prepend special folders res = SpecialFolders.Nodes.Union(res); } if (specialtoken != null) { res = from n in res select new Serializable.TreeNode() { id = specialtoken + n.id.Substring(specialpath.Length), text = n.text, iconCls = n.iconCls, leaf = n.leaf, resolvedpath = n.id }; } info.OutputOK(res); } catch (Exception ex) { info.ReportClientError("Failed to process the path: " + ex.Message); } } private static IEnumerable ListFolderAsNodes(string entrypath, bool skipFiles) { //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; }; var systemIO = Library.Utility.Utility.IsClientLinux ? (Duplicati.Library.Snapshots.ISystemIO)new Duplicati.Library.Snapshots.SystemIOLinux() : (Duplicati.Library.Snapshots.ISystemIO)new Duplicati.Library.Snapshots.SystemIOWindows(); foreach (var s in System.IO.Directory.EnumerateFileSystemEntries(entrypath)) { Serializable.TreeNode tn = null; try { var attr = systemIO.GetFileAttributes(s); //var isSymlink = (attr & FileAttributes.ReparsePoint) != 0; var isFolder = (attr & FileAttributes.Directory) != 0; var isFile = !isFolder; //var isHidden = (attr & FileAttributes.Hidden) != 0; var accesible = isFile || canAccess(s); var isLeaf = isFile || !accesible || isEmptyFolder(s); var rawid = isFolder ? Library.Utility.Utility.AppendDirSeparator(s) : s; if (!skipFiles || isFolder) tn = new Serializable.TreeNode() { id = rawid, text = systemIO.PathGetFileName(s), iconCls = isFolder ? (accesible ? "x-tree-icon-parent" : "x-tree-icon-locked") : "x-tree-icon-leaf", leaf = isLeaf }; } catch { } if (tn != null) yield return tn; } } 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/Server/WebServer/RESTMethods/Help.cs index 6df741260..616ada9be 100644 --- a/Duplicati/Server/WebServer/RESTMethods/Help.cs +++ b/Duplicati/Server/WebServer/RESTMethods/Help.cs @@ -1,4 +1,4 @@ -// Copyright (C) 2015, The Duplicati Team +// 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 diff --git a/Duplicati/Server/WebServer/RESTMethods/IRESTMethod.cs b/Duplicati/Server/WebServer/RESTMethods/IRESTMethod.cs index 51a60fcf2..ae2647b86 100644 --- a/Duplicati/Server/WebServer/RESTMethods/IRESTMethod.cs +++ b/Duplicati/Server/WebServer/RESTMethods/IRESTMethod.cs @@ -1,4 +1,4 @@ -// Copyright (C) 2015, The Duplicati Team +// 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 @@ -19,6 +19,6 @@ 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 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/Localizations.cs b/Duplicati/Server/WebServer/RESTMethods/Localizations.cs index fd7a69a69..d3cd0212b 100644 --- a/Duplicati/Server/WebServer/RESTMethods/Localizations.cs +++ b/Duplicati/Server/WebServer/RESTMethods/Localizations.cs @@ -1,4 +1,4 @@ -// Copyright (C) 2015, The Duplicati Team +// 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 diff --git a/Duplicati/Server/WebServer/RESTMethods/LogData.cs b/Duplicati/Server/WebServer/RESTMethods/LogData.cs index e5652e410..177330575 100644 --- a/Duplicati/Server/WebServer/RESTMethods/LogData.cs +++ b/Duplicati/Server/WebServer/RESTMethods/LogData.cs @@ -1,4 +1,4 @@ -// Copyright (C) 2015, The Duplicati Team +// 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 @@ -19,7 +19,7 @@ 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.InvariantCultureIgnoreCase)) { var input = info.Request.QueryString; var level_str = input["level"].Value ?? ""; var id_str = input["id"].Value ?? ""; 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)); } 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[])), }; } } + { public void GET(string key, RequestInfo info) { if ("poll".Equals(key, StringComparison.InvariantCultureIgnoreCase)) { var input = info.Request.QueryString; var level_str = input["level"].Value ?? ""; var id_str = input["id"].Value ?? ""; 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)); } 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[])), }; } } } } diff --git a/Duplicati/Server/WebServer/RESTMethods/Notification.cs b/Duplicati/Server/WebServer/RESTMethods/Notification.cs index bd44c3878..24be2c1da 100644 --- a/Duplicati/Server/WebServer/RESTMethods/Notification.cs +++ b/Duplicati/Server/WebServer/RESTMethods/Notification.cs @@ -1,4 +1,4 @@ -// Copyright (C) 2015, The Duplicati Team +// 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 diff --git a/Duplicati/Server/WebServer/RESTMethods/Notifications.cs b/Duplicati/Server/WebServer/RESTMethods/Notifications.cs index d35d9577b..0a934d64c 100644 --- a/Duplicati/Server/WebServer/RESTMethods/Notifications.cs +++ b/Duplicati/Server/WebServer/RESTMethods/Notifications.cs @@ -1,4 +1,4 @@ -// Copyright (C) 2015, The Duplicati Team +// 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 diff --git a/Duplicati/Server/WebServer/RESTMethods/ProgressState.cs b/Duplicati/Server/WebServer/RESTMethods/ProgressState.cs index 723cf811b..b21ead7c6 100644 --- a/Duplicati/Server/WebServer/RESTMethods/ProgressState.cs +++ b/Duplicati/Server/WebServer/RESTMethods/ProgressState.cs @@ -1,4 +1,4 @@ -// Copyright (C) 2015, The Duplicati Team +// 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 diff --git a/Duplicati/Server/WebServer/RESTMethods/RemoteOperation.cs b/Duplicati/Server/WebServer/RESTMethods/RemoteOperation.cs index 9148d1f0c..b7a370289 100644 --- a/Duplicati/Server/WebServer/RESTMethods/RemoteOperation.cs +++ b/Duplicati/Server/WebServer/RESTMethods/RemoteOperation.cs @@ -1,4 +1,4 @@ -// Copyright (C) 2015, The Duplicati Team +// 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 diff --git a/Duplicati/Server/WebServer/RESTMethods/RequestInfo.cs b/Duplicati/Server/WebServer/RESTMethods/RequestInfo.cs index 1b7e42383..69269d37c 100644 --- a/Duplicati/Server/WebServer/RESTMethods/RequestInfo.cs +++ b/Duplicati/Server/WebServer/RESTMethods/RequestInfo.cs @@ -1,4 +1,4 @@ -// Copyright (C) 2015, The Duplicati Team +// 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 @@ -19,7 +19,7 @@ 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 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 = System.Net.HttpStatusCode.InternalServerError; 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 = Request.Method.ToUpper() == "POST" ? 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"); isError = true; return false; } TimeSpan ts; try { ts = Library.Utility.Timeparser.ParseTimeSpan(input["duration"].Value); } catch (Exception ex) { ReportClientError("Invalid duration: " + ex.Message); 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"); 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/Server/WebServer/RESTMethods/ServerSetting.cs index 40418eca2..a276d8d73 100644 --- a/Duplicati/Server/WebServer/RESTMethods/ServerSetting.cs +++ b/Duplicati/Server/WebServer/RESTMethods/ServerSetting.cs @@ -1,4 +1,4 @@ -// Copyright (C) 2015, The Duplicati Team +// 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 @@ -14,12 +14,48 @@ // 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; +using System.Collections.Generic; namespace Duplicati.Server.WebServer.RESTMethods { public class ServerSetting : IRESTMethodGET, IRESTMethodPUT, IRESTMethodDocumented - { public void GET(string key, RequestInfo info) { var prop = typeof(Database.ApplicationSettings).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) { var prop = typeof(Database.ApplicationSettings).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)) }; } } + { + public void GET(string key, RequestInfo info) + { + var prop = typeof(Database.ApplicationSettings).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) + { + var prop = typeof(Database.ApplicationSettings).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)) + }; + } + } } } diff --git a/Duplicati/Server/WebServer/RESTMethods/ServerSettings.cs b/Duplicati/Server/WebServer/RESTMethods/ServerSettings.cs index 3945feebd..3183dff31 100644 --- a/Duplicati/Server/WebServer/RESTMethods/ServerSettings.cs +++ b/Duplicati/Server/WebServer/RESTMethods/ServerSettings.cs @@ -1,4 +1,4 @@ -// Copyright (C) 2015, The Duplicati Team +// 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 @@ -14,10 +14,72 @@ // 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 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) { var adv_props = from n in Program.DataConnection.GetSettings(Database.Connection.APP_SETTINGS_ID) select new KeyValuePair(n.Name, n.Value); info.OutputOK(adv_props.ToDictionary(x => x.Key, x => x.Value)); } 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"); return; } Dictionary data = null; try { data = Serializer.Deserialize>(new StringReader(str)); if (data == null) { info.ReportClientError("Data object had no entry"); return; } Program.DataConnection.ApplicationSettings.UpdateSettings(data, false); info.OutputOK(); } catch (Exception ex) { if (data == null) info.ReportClientError(string.Format("Unable to parse data object: {0}", ex.Message)); else info.ReportClientError(string.Format("Unable to save settings: {0}", ex.Message)); } } 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(ApplicationSettings)) }; } } } + public class ServerSettings : IRESTMethodGET, IRESTMethodPATCH, IRESTMethodDocumented + { + public void GET(string key, RequestInfo info) + { + var adv_props = from n in Program.DataConnection.GetSettings(Database.Connection.APP_SETTINGS_ID) + select new KeyValuePair(n.Name, n.Value); + + info.OutputOK(adv_props.ToDictionary(x => x.Key, x => x.Value)); + } + + 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"); + return; + } + + Dictionary data = null; + try + { + data = Serializer.Deserialize>(new StringReader(str)); + if (data == null) + { + info.ReportClientError("Data object had no entry"); + return; + } + + Program.DataConnection.ApplicationSettings.UpdateSettings(data, false); + + info.OutputOK(); + } + catch (Exception ex) + { + if (data == null) + info.ReportClientError(string.Format("Unable to parse data object: {0}", ex.Message)); + else + info.ReportClientError(string.Format("Unable to save settings: {0}", ex.Message)); + } + } + + 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(ApplicationSettings)) + }; + } + } + } } diff --git a/Duplicati/Server/WebServer/RESTMethods/ServerState.cs b/Duplicati/Server/WebServer/RESTMethods/ServerState.cs index 50c74e756..586f23647 100644 --- a/Duplicati/Server/WebServer/RESTMethods/ServerState.cs +++ b/Duplicati/Server/WebServer/RESTMethods/ServerState.cs @@ -1,4 +1,4 @@ -// Copyright (C) 2015, The Duplicati Team +// 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 @@ -14,11 +14,86 @@ // 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; +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 eventnotifyer 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); 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)) }; } } } + { + 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 eventnotifyer 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); + 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)) + }; + } + } + } } diff --git a/Duplicati/Server/WebServer/RESTMethods/SystemInfo.cs b/Duplicati/Server/WebServer/RESTMethods/SystemInfo.cs index ee5a4ec82..24114dfd5 100644 --- a/Duplicati/Server/WebServer/RESTMethods/SystemInfo.cs +++ b/Duplicati/Server/WebServer/RESTMethods/SystemInfo.cs @@ -1,4 +1,4 @@ -// Copyright (C) 2015, The Duplicati Team +// 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 @@ -14,10 +14,69 @@ // 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 System; +using System.Linq; +using System.Collections.Generic; +using Duplicati.Library.Interface; namespace Duplicati.Server.WebServer.RESTMethods { public class SystemInfo : IRESTMethodGET, IRESTMethodDocumented - { public string Description { get { return "Gets various system properties"; } } public IEnumerable> Types { get { return new KeyValuePair[] { new KeyValuePair(HttpServer.Method.Get, SystemData.GetType()) }; } } public void GET(string key, RequestInfo info) { info.BodyWriter.OutputOK(SystemData); } private static object SystemData { get { return new { APIVersion = 1, PasswordPlaceholder = Duplicati.Server.WebServer.Server.PASSWORD_PLACEHOLDER, ServerVersion = System.Reflection.Assembly.GetExecutingAssembly().GetName().Version.ToString(), ServerVersionName = Duplicati.License.VersionNumbers.Version, ServerTime = DateTime.Now, OSType = Library.Utility.Utility.IsClientLinux ? (Library.Utility.Utility.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, 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, UsingAlternateUpdateURLs = Duplicati.Library.AutoUpdater.AutoUpdateSettings.UsesAlternateURLs, LogLevels = Enum.GetNames(typeof(Duplicati.Library.Logging.LogMessageType)) }; } } } } + { + public string Description { get { return "Gets various system properties"; } } + public IEnumerable> Types + { + get + { + return new KeyValuePair[] { + new KeyValuePair(HttpServer.Method.Get, SystemData.GetType()) + }; + } + } + + public void GET(string key, RequestInfo info) + { + info.BodyWriter.OutputOK(SystemData); + } + + private static object SystemData + { + get + { + return new + { + APIVersion = 1, + PasswordPlaceholder = Duplicati.Server.WebServer.Server.PASSWORD_PLACEHOLDER, + ServerVersion = System.Reflection.Assembly.GetExecutingAssembly().GetName().Version.ToString(), + ServerVersionName = Duplicati.License.VersionNumbers.Version, + ServerTime = DateTime.Now, + OSType = Library.Utility.Utility.IsClientLinux ? (Library.Utility.Utility.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, + 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, + UsingAlternateUpdateURLs = Duplicati.Library.AutoUpdater.AutoUpdateSettings.UsesAlternateURLs, + LogLevels = Enum.GetNames(typeof(Duplicati.Library.Logging.LogMessageType)) + }; + } + } + } +} diff --git a/Duplicati/Server/WebServer/RESTMethods/SystemWideSettings.cs b/Duplicati/Server/WebServer/RESTMethods/SystemWideSettings.cs index 9da0443a7..27d813110 100644 --- a/Duplicati/Server/WebServer/RESTMethods/SystemWideSettings.cs +++ b/Duplicati/Server/WebServer/RESTMethods/SystemWideSettings.cs @@ -1,4 +1,4 @@ -// Copyright (C) 2015, The Duplicati Team +// 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 diff --git a/Duplicati/Server/WebServer/RESTMethods/Tags.cs b/Duplicati/Server/WebServer/RESTMethods/Tags.cs index 98e9a8ef3..856a9de45 100644 --- a/Duplicati/Server/WebServer/RESTMethods/Tags.cs +++ b/Duplicati/Server/WebServer/RESTMethods/Tags.cs @@ -1,4 +1,4 @@ -// Copyright (C) 2015, The Duplicati Team +// 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 diff --git a/Duplicati/Server/WebServer/RESTMethods/Task.cs b/Duplicati/Server/WebServer/RESTMethods/Task.cs index 3b13525e6..d3548e20c 100644 --- a/Duplicati/Server/WebServer/RESTMethods/Task.cs +++ b/Duplicati/Server/WebServer/RESTMethods/Task.cs @@ -1,4 +1,4 @@ -// Copyright (C) 2015, The Duplicati Team +// 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 diff --git a/Duplicati/Server/WebServer/RESTMethods/Tasks.cs b/Duplicati/Server/WebServer/RESTMethods/Tasks.cs index cdcd82e97..90e5f6d91 100644 --- a/Duplicati/Server/WebServer/RESTMethods/Tasks.cs +++ b/Duplicati/Server/WebServer/RESTMethods/Tasks.cs @@ -1,4 +1,4 @@ -// Copyright (C) 2015, The Duplicati Team +// 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 diff --git a/Duplicati/Server/WebServer/RESTMethods/UISettings.cs b/Duplicati/Server/WebServer/RESTMethods/UISettings.cs index 13db3314b..0cadb8451 100644 --- a/Duplicati/Server/WebServer/RESTMethods/UISettings.cs +++ b/Duplicati/Server/WebServer/RESTMethods/UISettings.cs @@ -1,4 +1,4 @@ -// Copyright (C) 2015, The Duplicati Team +// 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 diff --git a/Duplicati/Server/WebServer/RESTMethods/Updates.cs b/Duplicati/Server/WebServer/RESTMethods/Updates.cs index ceeb6f7dc..e882ad211 100644 --- a/Duplicati/Server/WebServer/RESTMethods/Updates.cs +++ b/Duplicati/Server/WebServer/RESTMethods/Updates.cs @@ -1,4 +1,4 @@ -// Copyright (C) 2015, The Duplicati Team +// 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 diff --git a/Duplicati/Server/WebServer/RESTMethods/WebModule.cs b/Duplicati/Server/WebServer/RESTMethods/WebModule.cs index 1e7823d8a..108353938 100644 --- a/Duplicati/Server/WebServer/RESTMethods/WebModule.cs +++ b/Duplicati/Server/WebServer/RESTMethods/WebModule.cs @@ -1,4 +1,4 @@ -// Copyright (C) 2015, The Duplicati Team +// 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 diff --git a/Duplicati/Server/WebServer/RESTMethods/WebModules.cs b/Duplicati/Server/WebServer/RESTMethods/WebModules.cs index e48fd337a..e65b98335 100644 --- a/Duplicati/Server/WebServer/RESTMethods/WebModules.cs +++ b/Duplicati/Server/WebServer/RESTMethods/WebModules.cs @@ -1,4 +1,4 @@ -// Copyright (C) 2015, The Duplicati Team +// 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 diff --git a/Duplicati/UnitTest/BasicSetupHelper.cs b/Duplicati/UnitTest/BasicSetupHelper.cs index 10458f798..8f265f4cf 100644 --- a/Duplicati/UnitTest/BasicSetupHelper.cs +++ b/Duplicati/UnitTest/BasicSetupHelper.cs @@ -1,4 +1,4 @@ -// Copyright (C) 2015, The Duplicati Team +// 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 diff --git a/Duplicati/UnitTest/CommandLineOperationsTests.cs b/Duplicati/UnitTest/CommandLineOperationsTests.cs index 11a056ccc..2f4fe3a51 100644 --- a/Duplicati/UnitTest/CommandLineOperationsTests.cs +++ b/Duplicati/UnitTest/CommandLineOperationsTests.cs @@ -1,4 +1,4 @@ -// Copyright (C) 2015, The Duplicati Team +// 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 diff --git a/Duplicati/UnitTest/GeneralBlackBoxTesting.cs b/Duplicati/UnitTest/GeneralBlackBoxTesting.cs index f782be494..567339481 100644 --- a/Duplicati/UnitTest/GeneralBlackBoxTesting.cs +++ b/Duplicati/UnitTest/GeneralBlackBoxTesting.cs @@ -1,4 +1,4 @@ -// Copyright (C) 2015, The Duplicati Team +// 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 @@ -21,10 +21,10 @@ namespace Duplicati.UnitTest { [TestFixture] public class GeneralBlackBoxTesting - { private static readonly string SOURCE_FOLDERS = TestUtils.ExpandString(Path.Combine("~", "testdata", "DSMCBE")); protected IEnumerable TestFolders { get { var rx = new System.Text.RegularExpressions.Regex("r(?\\d+)"); return from n in Directory.EnumerateDirectories(SOURCE_FOLDERS) let m = rx.Match(n) where m.Success orderby int.Parse(m.Groups["number"].Value) select n; } } protected Dictionary TestOptions { get { return TestUtils.DefaultOptions; } } protected string TestTarget { get { var x = TestUtils.GetDefaultTarget("x"); return x == "x" ? null : x; } } + { private static readonly string SOURCE_FOLDERS = TestUtils.ExpandString(Path.Combine("~", "testdata", "DSMCBE")); protected IEnumerable TestFolders { get { var rx = new System.Text.RegularExpressions.Regex("r(?\\d+)"); return from n in Directory.EnumerateDirectories(SOURCE_FOLDERS) let m = rx.Match(n) where m.Success orderby int.Parse(m.Groups["number"].Value) select n; } } protected Dictionary TestOptions { get { return TestUtils.DefaultOptions; } } protected string TestTarget { get { var x = TestUtils.GetDefaultTarget("x"); return x == "x" ? null : x; } } [Test] public void TestWithSVNShort() - { SVNCheckoutTest.RunTest(TestFolders.Take(5).ToArray(), TestOptions, TestTarget); } + { SVNCheckoutTest.RunTest(TestFolders.Take(5).ToArray(), TestOptions, TestTarget); } [Test] public void TestWithSVNLong() { SVNCheckoutTest.RunTest(TestFolders.ToArray(), TestOptions, TestTarget); } [Test] public void TestWithErrors() { var u = new Library.Utility.Uri(TestUtils.GetDefaultTarget()); RandomErrorBackend.WrappedBackend = u.Scheme; var target = u.SetScheme(new RandomErrorBackend().ProtocolKey).ToString(); SVNCheckoutTest.RunTest(TestFolders.Take(5).ToArray(), TestOptions, target); } } } diff --git a/Duplicati/UnitTest/Issue1410.cs b/Duplicati/UnitTest/Issue1410.cs index aefa5e1a6..7cc7d6ef5 100644 --- a/Duplicati/UnitTest/Issue1410.cs +++ b/Duplicati/UnitTest/Issue1410.cs @@ -1,4 +1,4 @@ -// Copyright (C) 2015, The Duplicati Team +// 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 @@ -19,7 +19,7 @@ using System; using System.IO; using NUnit.Framework; using System.Linq; using S namespace Duplicati.UnitTest { [TestFixture] public class Issue1410 : BasicSetupHelper - { [TestFixtureSetUp()] public override void PrepareSourceData() { base.PrepareSourceData(); Directory.CreateDirectory(DATAFOLDER); Directory.CreateDirectory(TARGETFOLDER); } [Test] public void RunCommands() { var data = new byte[1024 * 1024 * 10]; //File.WriteAllText(Path.Combine(DATAFOLDER, "a"), "hi"); File.WriteAllBytes(Path.Combine(DATAFOLDER, "a"), data); using(var c = new Library.Main.Controller("file://" + TARGETFOLDER, TestOptions, null)) c.Backup(new string[] { DATAFOLDER }); new Random().NextBytes(data); //File.WriteAllText(Path.Combine(DATAFOLDER, "b"), "there"); File.WriteAllBytes(Path.Combine(DATAFOLDER, "b"), data); using(var c = new Library.Main.Controller("file://" + TARGETFOLDER, TestOptions, null)) c.Backup(new string[] { DATAFOLDER }); using(var c = new Library.Main.Controller("file://" + TARGETFOLDER, TestOptions.Expand(new { version = 0 }), null)) { var r = c.List("*"); Console.WriteLine("Newest before deleting:"); Console.WriteLine(string.Join(Environment.NewLine, r.Files.Select(x => x.Path))); Assert.AreEqual(3, r.Files.Count()); } using(var c = new Library.Main.Controller("file://" + TARGETFOLDER, TestOptions.Expand(new { version = 0, no_local_db = true }), null)) { var r = c.List("*"); Console.WriteLine("Newest without db:"); Console.WriteLine(string.Join(Environment.NewLine, r.Files.Select(x => x.Path))); Assert.AreEqual(3, r.Files.Count()); } File.Delete(DBFILE); using(var c = new Library.Main.Controller("file://" + TARGETFOLDER, TestOptions, null)) c.Repair(); using(var c = new Library.Main.Controller("file://" + TARGETFOLDER, TestOptions, null)) Assert.AreEqual(c.List().Filesets.Count(), 2); using(var c = new Library.Main.Controller("file://" + TARGETFOLDER, TestOptions.Expand(new { version = 1 }), null)) { var r = c.List("*"); Console.WriteLine("Oldest after delete:"); Console.WriteLine(string.Join(Environment.NewLine, r.Files.Select(x => x.Path))); Assert.AreEqual(2, r.Files.Count()); } using(var c = new Library.Main.Controller("file://" + TARGETFOLDER, TestOptions.Expand(new { version = 0 }), null)) { var r = c.List("*"); Console.WriteLine("Newest after delete:"); Console.WriteLine(string.Join(Environment.NewLine, r.Files.Select(x => x.Path))); Assert.AreEqual(3, r.Files.Count()); } } + { [TestFixtureSetUp()] public override void PrepareSourceData() { base.PrepareSourceData(); Directory.CreateDirectory(DATAFOLDER); Directory.CreateDirectory(TARGETFOLDER); } [Test] public void RunCommands() { var data = new byte[1024 * 1024 * 10]; //File.WriteAllText(Path.Combine(DATAFOLDER, "a"), "hi"); File.WriteAllBytes(Path.Combine(DATAFOLDER, "a"), data); using(var c = new Library.Main.Controller("file://" + TARGETFOLDER, TestOptions, null)) c.Backup(new string[] { DATAFOLDER }); new Random().NextBytes(data); //File.WriteAllText(Path.Combine(DATAFOLDER, "b"), "there"); File.WriteAllBytes(Path.Combine(DATAFOLDER, "b"), data); using(var c = new Library.Main.Controller("file://" + TARGETFOLDER, TestOptions, null)) c.Backup(new string[] { DATAFOLDER }); using(var c = new Library.Main.Controller("file://" + TARGETFOLDER, TestOptions.Expand(new { version = 0 }), null)) { var r = c.List("*"); Console.WriteLine("Newest before deleting:"); Console.WriteLine(string.Join(Environment.NewLine, r.Files.Select(x => x.Path))); Assert.AreEqual(3, r.Files.Count()); } using(var c = new Library.Main.Controller("file://" + TARGETFOLDER, TestOptions.Expand(new { version = 0, no_local_db = true }), null)) { var r = c.List("*"); Console.WriteLine("Newest without db:"); Console.WriteLine(string.Join(Environment.NewLine, r.Files.Select(x => x.Path))); Assert.AreEqual(3, r.Files.Count()); } File.Delete(DBFILE); using(var c = new Library.Main.Controller("file://" + TARGETFOLDER, TestOptions, null)) c.Repair(); using(var c = new Library.Main.Controller("file://" + TARGETFOLDER, TestOptions, null)) Assert.AreEqual(c.List().Filesets.Count(), 2); using(var c = new Library.Main.Controller("file://" + TARGETFOLDER, TestOptions.Expand(new { version = 1 }), null)) { var r = c.List("*"); Console.WriteLine("Oldest after delete:"); Console.WriteLine(string.Join(Environment.NewLine, r.Files.Select(x => x.Path))); Assert.AreEqual(2, r.Files.Count()); } using(var c = new Library.Main.Controller("file://" + TARGETFOLDER, TestOptions.Expand(new { version = 0 }), null)) { var r = c.List("*"); Console.WriteLine("Newest after delete:"); Console.WriteLine(string.Join(Environment.NewLine, r.Files.Select(x => x.Path))); Assert.AreEqual(3, r.Files.Count()); } } } } diff --git a/Duplicati/UnitTest/RandomErrorBackend.cs b/Duplicati/UnitTest/RandomErrorBackend.cs index 43aeeaab7..6f14115c5 100644 --- a/Duplicati/UnitTest/RandomErrorBackend.cs +++ b/Duplicati/UnitTest/RandomErrorBackend.cs @@ -1,4 +1,4 @@ -// Copyright (C) 2015, The Duplicati Team +// 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 @@ -19,10 +19,10 @@ using System; using Duplicati.Library.Interface; using System.Collections.Generi namespace Duplicati.UnitTest { public class RandomErrorBackend : IBackend, IStreamingBackend - { static RandomErrorBackend() { WrappedBackend = "file"; } private static Random random = new Random(42); public static string WrappedBackend { get; set; } private IStreamingBackend m_backend; + { static RandomErrorBackend() { WrappedBackend = "file"; } private static Random random = new Random(42); public static string WrappedBackend { get; set; } private IStreamingBackend m_backend; public RandomErrorBackend() { - } public RandomErrorBackend(string url, Dictionary options) { var u = new Library.Utility.Uri(url).SetScheme(WrappedBackend).ToString(); m_backend = (IStreamingBackend)Library.DynamicLoader.BackendLoader.GetBackend(u, options); } private void ThrowErrorRandom() { if (random.NextDouble() > 0.90) throw new Exception("Random upload failure"); } + } public RandomErrorBackend(string url, Dictionary options) { var u = new Library.Utility.Uri(url).SetScheme(WrappedBackend).ToString(); m_backend = (IStreamingBackend)Library.DynamicLoader.BackendLoader.GetBackend(u, options); } private void ThrowErrorRandom() { if (random.NextDouble() > 0.90) throw new Exception("Random upload failure"); } #region IStreamingBackend implementation public void Put(string remotename, Stream stream) { var uploadError = random.NextDouble() > 0.9; using(var f = new Library.Utility.ProgressReportingStream(stream, stream.Length, x => { if (uploadError && stream.Position > stream.Length / 2) throw new Exception("Random upload failure"); })) m_backend.Put(remotename, f); ThrowErrorRandom(); } public void Get(string remotename, Stream stream) { ThrowErrorRandom(); m_backend.Get(remotename, stream); ThrowErrorRandom(); } #endregion #region IBackend implementation public List List() { return m_backend.List(); } public void Put(string remotename, string filename) { ThrowErrorRandom(); m_backend.Put(remotename, filename); ThrowErrorRandom(); } public void Get(string remotename, string filename) { ThrowErrorRandom(); m_backend.Get(remotename, filename); ThrowErrorRandom(); } public void Delete(string remotename) { ThrowErrorRandom(); m_backend.Delete(remotename); ThrowErrorRandom(); } public void Test() { m_backend.Test(); } public void CreateFolder() { m_backend.CreateFolder(); } public string DisplayName { get { return "Random Error Backend"; } } public string ProtocolKey { get { return "randomerror"; } } public IList SupportedCommands { get { if (m_backend == null) try { return Duplicati.Library.DynamicLoader.BackendLoader.GetSupportedCommands(WrappedBackend + "://"); } catch { } return m_backend.SupportedCommands; } } public string Description { get { return "A testing backend that randomly fails"; } } #endregion #region IDisposable implementation public void Dispose() { if (m_backend != null) try { m_backend.Dispose(); } finally { m_backend = null; } } #endregion } } diff --git a/Duplicati/UnitTest/TestUtils.cs b/Duplicati/UnitTest/TestUtils.cs index f0313124b..2735be53e 100644 --- a/Duplicati/UnitTest/TestUtils.cs +++ b/Duplicati/UnitTest/TestUtils.cs @@ -1,4 +1,4 @@ -// Copyright (C) 2015, The Duplicati Team +// 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