From 3921f88e309824a467342c0cb0fdc0f431e6181f Mon Sep 17 00:00:00 2001 From: unknown Date: Tue, 9 Feb 2016 00:47:22 +0100 Subject: [PATCH 01/48] implemented functionality for changing the key for dictionary items --- .../settings/EditDictionaryItem.aspx.cs | 39 ++++++++++++++++--- src/umbraco.cms/businesslogic/Dictionary.cs | 6 +++ 2 files changed, 40 insertions(+), 5 deletions(-) diff --git a/src/Umbraco.Web/umbraco.presentation/umbraco/settings/EditDictionaryItem.aspx.cs b/src/Umbraco.Web/umbraco.presentation/umbraco/settings/EditDictionaryItem.aspx.cs index deb77f4bad..ca07004f44 100644 --- a/src/Umbraco.Web/umbraco.presentation/umbraco/settings/EditDictionaryItem.aspx.cs +++ b/src/Umbraco.Web/umbraco.presentation/umbraco/settings/EditDictionaryItem.aspx.cs @@ -25,6 +25,7 @@ namespace umbraco.settings protected uicontrols.TabView tbv = new uicontrols.TabView(); private System.Collections.ArrayList languageFields = new System.Collections.ArrayList(); private cms.businesslogic.Dictionary.DictionaryItem currentItem; + protected TextBox keyNameBox; protected void Page_Load(object sender, System.EventArgs e) { @@ -33,16 +34,16 @@ namespace umbraco.settings // Put user code to initialize the page here Panel1.hasMenu = true; Panel1.Text = ui.Text("editdictionary") + ": " + currentItem.key; - - uicontrols.Pane p = new uicontrols.Pane(); - var save = Panel1.Menu.NewButton(); + var save = Panel1.Menu.NewButton(); save.Text = ui.Text("save"); save.Click += save_Click; save.ToolTip = ui.Text("save"); save.ID = "save"; save.ButtonType = uicontrols.MenuButtonType.Primary; + uicontrols.Pane p = new uicontrols.Pane(); + Literal txt = new Literal(); txt.Text = "

" + ui.Text("dictionaryItem", "description", currentItem.key, base.getUser()) + "


"; p.addProperty(txt); @@ -63,7 +64,23 @@ namespace umbraco.settings } - if (!IsPostBack) + keyNameBox = new TextBox + { + ID = "editname-" + currentItem.id, + CssClass = "umbEditorTextField", + Text = currentItem.key + }; + + var txtChangeKey = new Literal + { + Text = "

 

" + + "

Change the key of the dictionary item. Carefull :)

" + }; + + p.addProperty(txtChangeKey); + p.addProperty(keyNameBox); + + if (!IsPostBack) { var path = BuildPath(currentItem); ClientTools @@ -71,7 +88,6 @@ namespace umbraco.settings .SyncTree(path, false); } - Panel1.Controls.Add(p); } @@ -92,8 +108,21 @@ namespace umbraco.settings currentItem.setValue(int.Parse(t.ID),t.Text); } } + + var newKey = keyNameBox.Text; + if (string.IsNullOrWhiteSpace(newKey) == false && newKey != currentItem.key) + { + currentItem.setKey(newKey); + + Panel1.title.InnerHtml = ui.Text("editdictionary") + ": " + currentItem.key; + + var path = BuildPath(currentItem); + ClientTools.SyncTree(path, true); + } + ClientTools.ShowSpeechBubble(speechBubbleIcon.save, ui.Text("speechBubbles", "dictionaryItemSaved"), ""); } + #region Web Form Designer generated code override protected void OnInit(EventArgs e) { diff --git a/src/umbraco.cms/businesslogic/Dictionary.cs b/src/umbraco.cms/businesslogic/Dictionary.cs index 8e76122175..206965168e 100644 --- a/src/umbraco.cms/businesslogic/Dictionary.cs +++ b/src/umbraco.cms/businesslogic/Dictionary.cs @@ -169,6 +169,12 @@ namespace umbraco.cms.businesslogic } } + public void setKey(string value) + { + key = value; + Save(); + } + public string Value(int languageId) { if (languageId == 0) From eec651c9d3d466d9f68afdc6b0757a0515a6fdfd Mon Sep 17 00:00:00 2001 From: unknown Date: Tue, 9 Feb 2016 00:47:22 +0100 Subject: [PATCH 02/48] implemented functionality for changing the key for dictionary items --- .../settings/EditDictionaryItem.aspx.cs | 39 ++++++++++++++++--- src/umbraco.cms/businesslogic/Dictionary.cs | 6 +++ 2 files changed, 40 insertions(+), 5 deletions(-) diff --git a/src/Umbraco.Web/umbraco.presentation/umbraco/settings/EditDictionaryItem.aspx.cs b/src/Umbraco.Web/umbraco.presentation/umbraco/settings/EditDictionaryItem.aspx.cs index deb77f4bad..ca07004f44 100644 --- a/src/Umbraco.Web/umbraco.presentation/umbraco/settings/EditDictionaryItem.aspx.cs +++ b/src/Umbraco.Web/umbraco.presentation/umbraco/settings/EditDictionaryItem.aspx.cs @@ -25,6 +25,7 @@ namespace umbraco.settings protected uicontrols.TabView tbv = new uicontrols.TabView(); private System.Collections.ArrayList languageFields = new System.Collections.ArrayList(); private cms.businesslogic.Dictionary.DictionaryItem currentItem; + protected TextBox keyNameBox; protected void Page_Load(object sender, System.EventArgs e) { @@ -33,16 +34,16 @@ namespace umbraco.settings // Put user code to initialize the page here Panel1.hasMenu = true; Panel1.Text = ui.Text("editdictionary") + ": " + currentItem.key; - - uicontrols.Pane p = new uicontrols.Pane(); - var save = Panel1.Menu.NewButton(); + var save = Panel1.Menu.NewButton(); save.Text = ui.Text("save"); save.Click += save_Click; save.ToolTip = ui.Text("save"); save.ID = "save"; save.ButtonType = uicontrols.MenuButtonType.Primary; + uicontrols.Pane p = new uicontrols.Pane(); + Literal txt = new Literal(); txt.Text = "

" + ui.Text("dictionaryItem", "description", currentItem.key, base.getUser()) + "


"; p.addProperty(txt); @@ -63,7 +64,23 @@ namespace umbraco.settings } - if (!IsPostBack) + keyNameBox = new TextBox + { + ID = "editname-" + currentItem.id, + CssClass = "umbEditorTextField", + Text = currentItem.key + }; + + var txtChangeKey = new Literal + { + Text = "

 

" + + "

Change the key of the dictionary item. Carefull :)

" + }; + + p.addProperty(txtChangeKey); + p.addProperty(keyNameBox); + + if (!IsPostBack) { var path = BuildPath(currentItem); ClientTools @@ -71,7 +88,6 @@ namespace umbraco.settings .SyncTree(path, false); } - Panel1.Controls.Add(p); } @@ -92,8 +108,21 @@ namespace umbraco.settings currentItem.setValue(int.Parse(t.ID),t.Text); } } + + var newKey = keyNameBox.Text; + if (string.IsNullOrWhiteSpace(newKey) == false && newKey != currentItem.key) + { + currentItem.setKey(newKey); + + Panel1.title.InnerHtml = ui.Text("editdictionary") + ": " + currentItem.key; + + var path = BuildPath(currentItem); + ClientTools.SyncTree(path, true); + } + ClientTools.ShowSpeechBubble(speechBubbleIcon.save, ui.Text("speechBubbles", "dictionaryItemSaved"), ""); } + #region Web Form Designer generated code override protected void OnInit(EventArgs e) { diff --git a/src/umbraco.cms/businesslogic/Dictionary.cs b/src/umbraco.cms/businesslogic/Dictionary.cs index 8e76122175..206965168e 100644 --- a/src/umbraco.cms/businesslogic/Dictionary.cs +++ b/src/umbraco.cms/businesslogic/Dictionary.cs @@ -169,6 +169,12 @@ namespace umbraco.cms.businesslogic } } + public void setKey(string value) + { + key = value; + Save(); + } + public string Value(int languageId) { if (languageId == 0) From 9427979b1c5b4a7ce0525779afe0cba22c623b54 Mon Sep 17 00:00:00 2001 From: Mads Krohn Date: Tue, 16 Feb 2016 00:06:56 +0100 Subject: [PATCH 03/48] Check if the new key already exists. Added error message. Enhanced error handling logic. --- .../settings/EditDictionaryItem.aspx.cs | 46 +++++++++++++------ 1 file changed, 33 insertions(+), 13 deletions(-) diff --git a/src/Umbraco.Web/umbraco.presentation/umbraco/settings/EditDictionaryItem.aspx.cs b/src/Umbraco.Web/umbraco.presentation/umbraco/settings/EditDictionaryItem.aspx.cs index ca07004f44..b0dd94c869 100644 --- a/src/Umbraco.Web/umbraco.presentation/umbraco/settings/EditDictionaryItem.aspx.cs +++ b/src/Umbraco.Web/umbraco.presentation/umbraco/settings/EditDictionaryItem.aspx.cs @@ -8,6 +8,7 @@ using System.Web.SessionState; using System.Web.UI; using System.Web.UI.WebControls; using System.Web.UI.HtmlControls; +using umbraco.cms.businesslogic; using umbraco.cms.presentation.Trees; using Umbraco.Core; using Umbraco.Core.IO; @@ -25,7 +26,8 @@ namespace umbraco.settings protected uicontrols.TabView tbv = new uicontrols.TabView(); private System.Collections.ArrayList languageFields = new System.Collections.ArrayList(); private cms.businesslogic.Dictionary.DictionaryItem currentItem; - protected TextBox keyNameBox; + protected TextBox boxChangeKey; + protected Label labelChangeKey; protected void Page_Load(object sender, System.EventArgs e) { @@ -64,21 +66,26 @@ namespace umbraco.settings } - keyNameBox = new TextBox + boxChangeKey = new TextBox { - ID = "editname-" + currentItem.id, + ID = "changeKey-" + currentItem.id, CssClass = "umbEditorTextField", Text = currentItem.key }; - var txtChangeKey = new Literal + labelChangeKey = new Label + { + ID = "changeKeyLabel", + CssClass = "text-error" + }; + + p.addProperty(new Literal { Text = "

 

" + "

Change the key of the dictionary item. Carefull :)

" - }; - - p.addProperty(txtChangeKey); - p.addProperty(keyNameBox); + }); + p.addProperty(boxChangeKey); + p.addProperty(labelChangeKey); if (!IsPostBack) { @@ -109,15 +116,28 @@ namespace umbraco.settings } } - var newKey = keyNameBox.Text; + labelChangeKey.Text = ""; + var newKey = boxChangeKey.Text; if (string.IsNullOrWhiteSpace(newKey) == false && newKey != currentItem.key) { - currentItem.setKey(newKey); + // key already exists, save but inform + if (Dictionary.DictionaryItem.hasKey(newKey) == true) + { + labelChangeKey.Text = "The key '" + newKey + "' already exists, sorry.."; + boxChangeKey.Text = currentItem.key; // reset the key + } + else + { + // set the new key + currentItem.setKey(newKey); - Panel1.title.InnerHtml = ui.Text("editdictionary") + ": " + currentItem.key; + // update the title with the new key + Panel1.title.InnerHtml = ui.Text("editdictionary") + ": " + newKey; - var path = BuildPath(currentItem); - ClientTools.SyncTree(path, true); + // sync the content tree + var path = BuildPath(currentItem); + ClientTools.SyncTree(path, true); + } } ClientTools.ShowSpeechBubble(speechBubbleIcon.save, ui.Text("speechBubbles", "dictionaryItemSaved"), ""); From 56492cd1b20360490ab4749f18bc31b8b7214b8a Mon Sep 17 00:00:00 2001 From: unknown Date: Tue, 9 Feb 2016 00:47:22 +0100 Subject: [PATCH 04/48] implemented functionality for changing the key for dictionary items --- .../settings/EditDictionaryItem.aspx.cs | 39 ++++++++++++++++--- src/umbraco.cms/businesslogic/Dictionary.cs | 6 +++ 2 files changed, 40 insertions(+), 5 deletions(-) diff --git a/src/Umbraco.Web/umbraco.presentation/umbraco/settings/EditDictionaryItem.aspx.cs b/src/Umbraco.Web/umbraco.presentation/umbraco/settings/EditDictionaryItem.aspx.cs index deb77f4bad..ca07004f44 100644 --- a/src/Umbraco.Web/umbraco.presentation/umbraco/settings/EditDictionaryItem.aspx.cs +++ b/src/Umbraco.Web/umbraco.presentation/umbraco/settings/EditDictionaryItem.aspx.cs @@ -25,6 +25,7 @@ namespace umbraco.settings protected uicontrols.TabView tbv = new uicontrols.TabView(); private System.Collections.ArrayList languageFields = new System.Collections.ArrayList(); private cms.businesslogic.Dictionary.DictionaryItem currentItem; + protected TextBox keyNameBox; protected void Page_Load(object sender, System.EventArgs e) { @@ -33,16 +34,16 @@ namespace umbraco.settings // Put user code to initialize the page here Panel1.hasMenu = true; Panel1.Text = ui.Text("editdictionary") + ": " + currentItem.key; - - uicontrols.Pane p = new uicontrols.Pane(); - var save = Panel1.Menu.NewButton(); + var save = Panel1.Menu.NewButton(); save.Text = ui.Text("save"); save.Click += save_Click; save.ToolTip = ui.Text("save"); save.ID = "save"; save.ButtonType = uicontrols.MenuButtonType.Primary; + uicontrols.Pane p = new uicontrols.Pane(); + Literal txt = new Literal(); txt.Text = "

" + ui.Text("dictionaryItem", "description", currentItem.key, base.getUser()) + "


"; p.addProperty(txt); @@ -63,7 +64,23 @@ namespace umbraco.settings } - if (!IsPostBack) + keyNameBox = new TextBox + { + ID = "editname-" + currentItem.id, + CssClass = "umbEditorTextField", + Text = currentItem.key + }; + + var txtChangeKey = new Literal + { + Text = "

 

" + + "

Change the key of the dictionary item. Carefull :)

" + }; + + p.addProperty(txtChangeKey); + p.addProperty(keyNameBox); + + if (!IsPostBack) { var path = BuildPath(currentItem); ClientTools @@ -71,7 +88,6 @@ namespace umbraco.settings .SyncTree(path, false); } - Panel1.Controls.Add(p); } @@ -92,8 +108,21 @@ namespace umbraco.settings currentItem.setValue(int.Parse(t.ID),t.Text); } } + + var newKey = keyNameBox.Text; + if (string.IsNullOrWhiteSpace(newKey) == false && newKey != currentItem.key) + { + currentItem.setKey(newKey); + + Panel1.title.InnerHtml = ui.Text("editdictionary") + ": " + currentItem.key; + + var path = BuildPath(currentItem); + ClientTools.SyncTree(path, true); + } + ClientTools.ShowSpeechBubble(speechBubbleIcon.save, ui.Text("speechBubbles", "dictionaryItemSaved"), ""); } + #region Web Form Designer generated code override protected void OnInit(EventArgs e) { diff --git a/src/umbraco.cms/businesslogic/Dictionary.cs b/src/umbraco.cms/businesslogic/Dictionary.cs index 8e76122175..206965168e 100644 --- a/src/umbraco.cms/businesslogic/Dictionary.cs +++ b/src/umbraco.cms/businesslogic/Dictionary.cs @@ -169,6 +169,12 @@ namespace umbraco.cms.businesslogic } } + public void setKey(string value) + { + key = value; + Save(); + } + public string Value(int languageId) { if (languageId == 0) From 330cd7a4092b1b81325bb550ec7372c16999a1c5 Mon Sep 17 00:00:00 2001 From: Mads Krohn Date: Tue, 16 Feb 2016 00:06:56 +0100 Subject: [PATCH 05/48] Check if the new key already exists. Added error message. Enhanced error handling logic. --- .../settings/EditDictionaryItem.aspx.cs | 46 +++++++++++++------ 1 file changed, 33 insertions(+), 13 deletions(-) diff --git a/src/Umbraco.Web/umbraco.presentation/umbraco/settings/EditDictionaryItem.aspx.cs b/src/Umbraco.Web/umbraco.presentation/umbraco/settings/EditDictionaryItem.aspx.cs index ca07004f44..b0dd94c869 100644 --- a/src/Umbraco.Web/umbraco.presentation/umbraco/settings/EditDictionaryItem.aspx.cs +++ b/src/Umbraco.Web/umbraco.presentation/umbraco/settings/EditDictionaryItem.aspx.cs @@ -8,6 +8,7 @@ using System.Web.SessionState; using System.Web.UI; using System.Web.UI.WebControls; using System.Web.UI.HtmlControls; +using umbraco.cms.businesslogic; using umbraco.cms.presentation.Trees; using Umbraco.Core; using Umbraco.Core.IO; @@ -25,7 +26,8 @@ namespace umbraco.settings protected uicontrols.TabView tbv = new uicontrols.TabView(); private System.Collections.ArrayList languageFields = new System.Collections.ArrayList(); private cms.businesslogic.Dictionary.DictionaryItem currentItem; - protected TextBox keyNameBox; + protected TextBox boxChangeKey; + protected Label labelChangeKey; protected void Page_Load(object sender, System.EventArgs e) { @@ -64,21 +66,26 @@ namespace umbraco.settings } - keyNameBox = new TextBox + boxChangeKey = new TextBox { - ID = "editname-" + currentItem.id, + ID = "changeKey-" + currentItem.id, CssClass = "umbEditorTextField", Text = currentItem.key }; - var txtChangeKey = new Literal + labelChangeKey = new Label + { + ID = "changeKeyLabel", + CssClass = "text-error" + }; + + p.addProperty(new Literal { Text = "

 

" + "

Change the key of the dictionary item. Carefull :)

" - }; - - p.addProperty(txtChangeKey); - p.addProperty(keyNameBox); + }); + p.addProperty(boxChangeKey); + p.addProperty(labelChangeKey); if (!IsPostBack) { @@ -109,15 +116,28 @@ namespace umbraco.settings } } - var newKey = keyNameBox.Text; + labelChangeKey.Text = ""; + var newKey = boxChangeKey.Text; if (string.IsNullOrWhiteSpace(newKey) == false && newKey != currentItem.key) { - currentItem.setKey(newKey); + // key already exists, save but inform + if (Dictionary.DictionaryItem.hasKey(newKey) == true) + { + labelChangeKey.Text = "The key '" + newKey + "' already exists, sorry.."; + boxChangeKey.Text = currentItem.key; // reset the key + } + else + { + // set the new key + currentItem.setKey(newKey); - Panel1.title.InnerHtml = ui.Text("editdictionary") + ": " + currentItem.key; + // update the title with the new key + Panel1.title.InnerHtml = ui.Text("editdictionary") + ": " + newKey; - var path = BuildPath(currentItem); - ClientTools.SyncTree(path, true); + // sync the content tree + var path = BuildPath(currentItem); + ClientTools.SyncTree(path, true); + } } ClientTools.ShowSpeechBubble(speechBubbleIcon.save, ui.Text("speechBubbles", "dictionaryItemSaved"), ""); From ce897509bae64ddc120fb6c720c1ed77cf077c1b Mon Sep 17 00:00:00 2001 From: Mads Krohn Date: Tue, 16 Feb 2016 10:44:43 +0100 Subject: [PATCH 06/48] added localization --- src/Umbraco.Web.UI/umbraco/config/lang/da.xml | 6 ++++++ src/Umbraco.Web.UI/umbraco/config/lang/en.xml | 6 ++++++ src/Umbraco.Web.UI/umbraco/config/lang/en_us.xml | 6 ++++++ .../umbraco/settings/EditDictionaryItem.aspx.cs | 14 ++++++++------ 4 files changed, 26 insertions(+), 6 deletions(-) diff --git a/src/Umbraco.Web.UI/umbraco/config/lang/da.xml b/src/Umbraco.Web.UI/umbraco/config/lang/da.xml index 446796dd88..f90d3a5ec6 100644 --- a/src/Umbraco.Web.UI/umbraco/config/lang/da.xml +++ b/src/Umbraco.Web.UI/umbraco/config/lang/da.xml @@ -236,6 +236,12 @@ Rediger de forskellige sprogversioner for ordbogselementet '%0%' herunder. Du tilføjer flere sprog under 'sprog' i menuen til venstre Kulturnavn + Her kan du ændre nøglen på ordbogselementet. + + + Indtast dit brugernavn diff --git a/src/Umbraco.Web.UI/umbraco/config/lang/en.xml b/src/Umbraco.Web.UI/umbraco/config/lang/en.xml index b14b101af7..c953e899cc 100644 --- a/src/Umbraco.Web.UI/umbraco/config/lang/en.xml +++ b/src/Umbraco.Web.UI/umbraco/config/lang/en.xml @@ -284,6 +284,12 @@ Edit the different language versions for the dictionary item '%0%' below
You can add additional languages under the 'languages' in the menu on the left ]]> Culture Name + Here you can change the key of the dictionary item. + + + Enter your username diff --git a/src/Umbraco.Web.UI/umbraco/config/lang/en_us.xml b/src/Umbraco.Web.UI/umbraco/config/lang/en_us.xml index 1cc40abd1d..012d7074e7 100644 --- a/src/Umbraco.Web.UI/umbraco/config/lang/en_us.xml +++ b/src/Umbraco.Web.UI/umbraco/config/lang/en_us.xml @@ -285,6 +285,12 @@ Edit the different language versions for the dictionary item '%0%' below
You can add additional languages under the 'languages' in the menu on the left ]]> Culture Name + Here you can change the key of the dictionary item. + + + Enter your username diff --git a/src/Umbraco.Web/umbraco.presentation/umbraco/settings/EditDictionaryItem.aspx.cs b/src/Umbraco.Web/umbraco.presentation/umbraco/settings/EditDictionaryItem.aspx.cs index b0dd94c869..c66a6ca37f 100644 --- a/src/Umbraco.Web/umbraco.presentation/umbraco/settings/EditDictionaryItem.aspx.cs +++ b/src/Umbraco.Web/umbraco.presentation/umbraco/settings/EditDictionaryItem.aspx.cs @@ -8,6 +8,7 @@ using System.Web.SessionState; using System.Web.UI; using System.Web.UI.WebControls; using System.Web.UI.HtmlControls; +using umbraco.BusinessLogic; using umbraco.cms.businesslogic; using umbraco.cms.presentation.Trees; using Umbraco.Core; @@ -28,10 +29,12 @@ namespace umbraco.settings private cms.businesslogic.Dictionary.DictionaryItem currentItem; protected TextBox boxChangeKey; protected Label labelChangeKey; + protected User currentUser; protected void Page_Load(object sender, System.EventArgs e) { currentItem = new cms.businesslogic.Dictionary.DictionaryItem(int.Parse(Request.QueryString["id"])); + currentUser = getUser(); // Put user code to initialize the page here Panel1.hasMenu = true; @@ -47,7 +50,7 @@ namespace umbraco.settings uicontrols.Pane p = new uicontrols.Pane(); Literal txt = new Literal(); - txt.Text = "

" + ui.Text("dictionaryItem", "description", currentItem.key, base.getUser()) + "


"; + txt.Text = "

" + ui.Text("dictionaryItem", "description", currentItem.key, currentUser) + "


"; p.addProperty(txt); foreach (cms.businesslogic.language.Language l in cms.businesslogic.language.Language.getAll) @@ -81,8 +84,7 @@ namespace umbraco.settings p.addProperty(new Literal { - Text = "

 

" + - "

Change the key of the dictionary item. Carefull :)

" + Text = "

 

" + ui.Text("dictionaryItem", "changeKey") + "

" }); p.addProperty(boxChangeKey); p.addProperty(labelChangeKey); @@ -116,15 +118,15 @@ namespace umbraco.settings } } - labelChangeKey.Text = ""; + labelChangeKey.Text = ""; // reset error text var newKey = boxChangeKey.Text; if (string.IsNullOrWhiteSpace(newKey) == false && newKey != currentItem.key) { // key already exists, save but inform if (Dictionary.DictionaryItem.hasKey(newKey) == true) { - labelChangeKey.Text = "The key '" + newKey + "' already exists, sorry.."; - boxChangeKey.Text = currentItem.key; // reset the key + labelChangeKey.Text = ui.Text("dictionaryItem", "changeKeyError", newKey, currentUser); + boxChangeKey.Text = currentItem.key; // reset key } else { From d8eaab2aa4f4e9207c1b3c569a8f3943ef7c3218 Mon Sep 17 00:00:00 2001 From: Mads Krohn Date: Tue, 16 Feb 2016 11:02:29 +0100 Subject: [PATCH 07/48] added a missing
to the danish translation. added current user to the changeKey text. --- src/Umbraco.Web.UI/umbraco/config/lang/da.xml | 4 +++- .../umbraco/settings/EditDictionaryItem.aspx.cs | 2 +- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/src/Umbraco.Web.UI/umbraco/config/lang/da.xml b/src/Umbraco.Web.UI/umbraco/config/lang/da.xml index f90d3a5ec6..9e24a6e167 100644 --- a/src/Umbraco.Web.UI/umbraco/config/lang/da.xml +++ b/src/Umbraco.Web.UI/umbraco/config/lang/da.xml @@ -234,7 +234,9 @@ Se Cache Item - Rediger de forskellige sprogversioner for ordbogselementet '%0%' herunder. Du tilføjer flere sprog under 'sprog' i menuen til venstre + Du tilføjer flere sprog under 'sprog' i menuen til venstre + ]]> Kulturnavn Her kan du ændre nøglen på ordbogselementet. diff --git a/src/Umbraco.Web/umbraco.presentation/umbraco/settings/EditDictionaryItem.aspx.cs b/src/Umbraco.Web/umbraco.presentation/umbraco/settings/EditDictionaryItem.aspx.cs index c66a6ca37f..dcece3d955 100644 --- a/src/Umbraco.Web/umbraco.presentation/umbraco/settings/EditDictionaryItem.aspx.cs +++ b/src/Umbraco.Web/umbraco.presentation/umbraco/settings/EditDictionaryItem.aspx.cs @@ -84,7 +84,7 @@ namespace umbraco.settings p.addProperty(new Literal { - Text = "

 

" + ui.Text("dictionaryItem", "changeKey") + "

" + Text = "

 

" + ui.Text("dictionaryItem", "changeKey", currentUser) + "

" }); p.addProperty(boxChangeKey); p.addProperty(labelChangeKey); From d74a30e4e405dca3ad970682d695c1c65b1f8d1e Mon Sep 17 00:00:00 2001 From: michael Date: Fri, 9 Sep 2016 13:17:53 +0200 Subject: [PATCH 08/48] Review and add translation labels to language file fr.xml --- src/Umbraco.Web.UI/umbraco/config/lang/fr.xml | 985 ++++++++++++------ 1 file changed, 662 insertions(+), 323 deletions(-) diff --git a/src/Umbraco.Web.UI/umbraco/config/lang/fr.xml b/src/Umbraco.Web.UI/umbraco/config/lang/fr.xml index 59021956a5..2e5ee51bac 100644 --- a/src/Umbraco.Web.UI/umbraco/config/lang/fr.xml +++ b/src/Umbraco.Web.UI/umbraco/config/lang/fr.xml @@ -26,7 +26,8 @@ Publier Dépublier Rafraîchir - Republier le site entier + Republier le site tout entier + Récupérer Permissions Version antérieure Envoyer pour publication @@ -39,13 +40,13 @@ Permission refusée. - Ajouter nouveau domaine + Ajouter un nouveau domaine Supprimer Noeud invalide. Domaine invalide. Domaine déjà assigné. Domaine - Langage + Langue Nouveau domaine '%0%' créé Domaine '%0%' supprimé Domaine '%0%' déjà assigné @@ -53,24 +54,25 @@ "https://www.example.com/".

Les domaines contenant un chemin d'un niveau sont autorisés, ex : "example.com/en". Pour autant, cela devrait être évité. Utilisez plutôt la gestion des noms d'hôte.]]>
Domaine '%0%' mis à jour - Editer les domaines + Editer les domaines actuels Hériter Culture - ou hériter la culture des noeuds parent. S'appliquera aussi'
- au noeud courant, à moins qu'un domaine soit appliqué aussi.]]>
+ ou hériter de la culture des noeuds parents. S'appliquera aussi
+ au noeud courant, à moins qu'un domaine ci-dessous soit aussi d'application.]]>
Domaines - Voir pour + Aperçu pour + Vider la sélection Choisir Choisir le répertoire courant Faire autre chose Gras Annuler l'indentation de paragraphe - Insérer champ de formulaire - Insérer une entête graphique + Insérer un champ de formulaire + Insérer un entête graphique Editer le HTML Indenter le paragraphe Italique @@ -78,42 +80,47 @@ Justifier à gauche Justifier à droite Insérer un lien - Insérer une ancre + Insérer un lien local (ancre) Liste à puces Liste numérique Insérer une macro Insérer une image + Retourner à la liste Editer les relations Sauver Sauver et publier Sauver et envoyer pour approbation + Sauver la mise en page de la liste Prévisualiser La prévisualisation est désactivée car aucun modèle n'a été assigné. Choisir un style Afficher les styles Insérer un tableau + Générer les modèles + Sauver et générer les modèles - Pour changer le type de document du contenu séléctionné, choisissez un type valide pour cet emplacement, qui soit conforme à la structure des types de documents. - Puis modifiez si nécessaire le mappage des propriétés du type actuel vers le nouveau, et cliquez sur Sauver + Pour changer le type de document du contenu séléctionné, faites d'abord un choix dans la liste des types valides à cet endroit. + Puis modifiez si nécessaire la correspondance des propriétés du type actuel vers le nouveau, et cliquez sur Sauver. Le contenu a été republié. Propriété actuelle Type actuel - Le type de document ne peut être changé, il n'y a pas d'alternatives valides pour cet emplacement. + Le type de document ne peut être changé car il n'y a pas d'alternative valide à cet endroit. Une alternative sera valide si elle est autorisée sous le parent du contenu sélectionné et si tous les éléments de contenu enfants existants peuvent être créés avec celle-ci. Type de document modifié - Mapper les propriétés - Mapper à la propriété + Faire correspondre les propriétés + Faire correspondre à la propriété Nouveau modèle Nouveau type aucun Contenu Choisir le nouveau Type de Document - Le type de document du contenu séléctionné a a bien été changé en [new type] et les propriétés suivantes mappées : + Le type de document du contenu séléctionné a bien été changé en [new type] et les correspondances de propriétés suivantes effectuées : en - Impossible de terminer le mappage des propriétés car une ou plus des propriétés ont plus de un mappage défini. - Seuls les types de documents valides pour cet emplacement sont affichés. + Impossible de terminer la correspondance des propriétés car une ou plusieurs propriétés ont plus d'une correspondance définie. + Seuls les types de documents valides à cet endroit sont affichés. + A été publié A propos de cette page Alias (comment décririez-vous l'image oralement) @@ -127,9 +134,10 @@ Type de Document Edition Expire le - Cet élément a été changé après la publication + Cet élément a été modifié après la publication Cet élément n'est pas publié Dernière publication + Il n'y a aucun élément à afficher Il n'y a aucun élément à afficher dans cette liste. Type de Média Lien vers des média(s) @@ -137,71 +145,85 @@ Rôle Type de membre Aucune date choisie - Titre de page + Titre de la page Propriétés - Ce document est publié mais n'est pas visible car son parent '%0%' est dépublié + Ce document est publié mais n'est pas visible car son parent '%0%' n'est pas publié Oups : ce document est publié mais n'est pas présent dans le cache (erreur interne Umbraco) + Oups: ce document est publié mais son url entrerait en collision avec le contenu %0% Publier Statut de publication Publié le Dépublié le Supprimer la date Ordre de tri mis à jour - Pour trier les noeuds, faites les glisser à l'aide de la souris ou cliquez sur les entêtes de colonne. Vous pouvez séléctionner plusieurs noeuds en gardant la touche "shift" ou "ctrl" enfoncée pendant votre séléction. + Pour trier les noeuds, faites-les simplement glisser à l'aide de la souris ou cliquez sur les entêtes de colonne. Vous pouvez séléctionner plusieurs noeuds en gardant la touche "shift" ou "ctrl" enfoncée pendant votre séléction. Statistiques Titre (optionnel) + Texte alternatif (optionnel) Type - Dépublié + Dépublier Dernière édition Date/heure à laquelle ce document a été édité - Supprimer fichier(s) + Supprimer le(s) fichier(s) Lien vers un document Membre du/des groupe(s) - Pas un membre du/des groupe(s) - Elements enfants + Pas membre du/des groupe(s) + Eléments enfants Cible Cliquez pour télécharger Faites glisser vos fichier ici... + Lien vers le média + ou cliquez ici pour choisir un fichier + Les seuls types de fichiers autorisés sont + Impossible de télécharger ce fichier, il n'a pas un type de fichier autorisé. + La taille maximum de fichier est + + + Créer un nouveau membre + Tous les membres Où voulez-vous créer le nouveau %0% Créer un élément sous Choisissez un type et un titre - "Types de documents".]]> - "Types de médias".]]> + "Types de documents".]]> + "Types de médias".]]> + Type de document sans modèle + Nouveau répertoire + Nouveau type de données Parcourir votre site - Cacher - Si Umbraco ne s'ouvre pas, vous devriez peut-être autoriser l'ouverture des popups pour ce site. + Si Umbraco ne s'ouvre pas, peut-être devez-vous autoriser l'ouverture des popups pour ce site. s'est ouvert dans une nouvelle fenêtre Redémarrer Visiter Bienvenue - Stay - Discard changes - You have unsaved changes - Are you sure you want to navigate away from this page? - you have unsaved changes + Rester + Invalider les changements + Vous avez des changements en cours + Etes-vous certain(e) de vouloir quitter cette page? - vous avez des changements en cours - Name + Nom Gérer les noms d'hôtes Fermer cette fenêtre - Êtes-vous sûr de vouloir supprimer - Êtes-vous sûr de vouloir désactiver + Êtes-vous certain(e) de vouloir supprimer + Êtes-vous certain(e) de vouloir désactiver Cochez cette case pour confirmer la suppression de %0% élément(s) - Êtes-vous sûr ? - Êtes-vous sûr ? + Êtes-vous certain(e)? + Êtes-vous certain(e)? Couper Editer une entrée du Dictionnaire - Editer le langage - Insérer une ancre + Modifier la langue + Insérer un lien local (ancre) Insérer un caractère - Insérer une entête graphique + Insérer un entête graphique Insérer une image Insérer un lien Insérer une macro @@ -209,75 +231,124 @@ Dernière modification Lien Lien interne : - Si vous utilisez des ancres, insérer # avant le lien - Ouvrir dans une nouvelle fenêtre ? + Si vous utilisez des ancres, insérez # au début du lien + Ouvrir dans une nouvelle fenêtre? Paramètres de macro Cette macro ne contient aucune propriété éditable Coller Editer les permissions pour Les éléments dans la corbeille sont en cours de suppression. Ne fermez pas cette fenêtre avant que cette opération soit terminée. La corbeille est maintenant vide - Les éléments supprimés de la corbeille sont supprimés définitivement - regexlib.com a actuellement des problèmes sur lesquels nous n'avons aucun contrôle. Excusez-nous pour le désagrément.]]> - Rechercher une expression régulière Search for a regular expression to add validation to a form field. Example: 'email, 'zip-code' 'url' + Les éléments supprimés de la corbeille seront supprimés définitivement + regexlib.com rencontre actuellement des problèmes sur lesquels nous n'avons aucun contrôle. Nous sommes sincèrement désolés pour le désagrément.]]> + Rechercher une expression régulière à ajouter pour la validation d'un champ de formulaire. Exemple: 'email, 'zip-code', 'url' Supprimer la macro - Champ requis + Champ obligatoire Le site a été réindéxé - Le cache du site a été mis à jour. Tous les contenus publiés sont à jour. Et tous les contenus dépubliés sont rendus invisibles. - Le cache du site va être mis à jour. Tous les contenus publiés seront à jour. Et tous les contenus dépubliés seront rendus invisibles. + Le cache du site a été mis à jour. Tous les contenus publiés sont maintenant à jour. Et tous les contenus dépubliés sont restés invisibles. + Le cache du site va être mis à jour. Tous les contenus publiés seront mis à jour. Et tous les contenus dépubliés resteront invisibles. Nombre de colonnes Nombre de lignes - Définir un placeholder id en mettant un ID sur votre placeholder vous pouvez injecter du contenu à cet endroit depuis vos modèles enfants, - en faisant référence à cet id au sein d'un élément <asp:content />.]]> - Séléctionnez un placeholder id depuis la liste ci-dessous. Vous pouvez seulement - choisir un ID depuis le modèle parent.]]> - Cliquez pour voir l'image en taille maximale - Séléctionner un élément + Définir un placeholder ID. En mettant un ID sur votre placeholder, vous pouvez injecter du contenu à cet endroit depuis les modèles enfants, + en faisant référence à cet ID au sein d'un élément <asp:content />.]]> + Séléctionnez un placeholder id dans la liste ci-dessous. Vous pouvez seulement + choisir un ID se trouvant dans le parent du modèle actuel.]]> + Cliquez sur l'image pour la voir en taille réelle + Sélectionner un élément Voir l'élément de cache + Créer un répertoire... + + Lier à l'original + La communauté la plus amicale + + Lier à la page + + Ouvre le document lié dans une nouvelle fenêtre ou un nouvel onglet + Ouvre le document lié dans l'entièreté de la fenêtre + Ouvre le document lié dans le conteneur parent + + Lier à un media + + Sélectionner le media + Sélectionner l'icône + Sélectionner l'élément + Sélectionner le lien + Sélectionner la macro + Sélectionner le contenu + Sélectionner le membre + Sélectionner le groupe de membres + + Il n'y a pas de paramètres pour cette macro + + Fournisseurs externes d'identification + Détails de l'exception + Trace d'exécution + Exception interne + + Liez votre + Enlevez votre + + compte + + Sélectionner un éditeur %0%' ci-dessous
Vous pouvez ajouter d'autres langages depuis le menu ci-dessous "Langages". + Editez les différentes versions de langues pour l'élément de dictionaire '%0%' ci-dessous.
Vous pouvez ajouter d'autres langues depuis le menu ci-dessous "Langues". ]]>
Nom de Culture Votre nom d'utilisateur Votre mot de passe + Confirmation de votre mot de passe Nommer %0%... Entrez un nom... + Libellé... + Entrez une description... Rechercher... Filtrer... + Ajouter des tags (appuyer sur enter entre chaque tag)... + Entrez votre email + + Autoriser à la racine + Seuls les Types de Contenu qui ont ceci coché peuvent être créés au niveau racine des arborescences de contenu et de media Types de noeuds enfants autorisés + Composition de Type de Documents Créer - Supprimer onglet + Supprimer l'onglet Description Nouvel onglet Onglet Miniature Activer la vue liste + Configure l'élément de contenu de sorte à afficher une liste de ses enfants que l'on peut trier et filtrer, les enfants ne seront pas affichés dans l'arborescence + Liste courante + Le type de donnée de la liste courante + Créer une liste personnalisée + Supprimer la liste personnalisée - Ajouter une prévaleur - Type de données en base de donées + Ajouter une valeur de base + Type de donnée en base de donées GUID du Property Editor Property editor Boutons Activer les paramètres avancés pour Activer le menu contextuel Taille maximale par défaut des images insérées - CSS relatives + CSS associées Afficher le libellé Largeur et hauteur - Vos données ont été sauvegardées, mais avant de publier votre page il y a des erreurs que vous devez corriger : - Le Membership Provider n'autorise pas le changement des mots de passe (EnablePasswordRetrieval doit être définit à true) + Vos données ont été sauvegardées, mais avant de pouvoir publier votre page, il y a des erreurs que vous devez corriger : + Le Membership Provider n'autorise pas le changement des mots de passe (EnablePasswordRetrieval doit être défini à true) %0% existe déjà - Il y a des erreurs : - Il y a des erreurs : + Des erreurs sont survenues : + Des erreurs sont survenues : Le mot de passe doit contenir un minimum de %0% caractères et contenir au moins %1% caractère(s) non-alphanumerique %0% doit être un entier Le champ %0% dans l'onglet %1% est obligatoire @@ -286,23 +357,30 @@ %0% n'est pas correctement formaté + Le serveur a retourné une erreur Le type de fichier spécifié n'est pas autorisé par l'administrateur - NOTE ! Même si CodeMirror est activé dans la configuration, il est désactivé sur Internet Explorer car il n'est pas stable sur ce navigateur. - Remplissez l'alias et le nom de la nouvelle propriété - Il y a un problème de droits de lecture/écriture sur un fichier ou dossier spécifique - Entrez un titre - Choisissez un type - Vous allez définir une taille d'image supérieure à sa taille d'origine. Êtes-vous sûr ? + NOTE ! Même si CodeMirror est activé dans la configuration, il est désactivé dans Internet Explorer car il n'est pas suffisamment stable dans ce navigateur. + Veuillez remplir l'alias et le nom de la nouvelle propriété! + Il y a un problème de droits en lecture/écriture sur un fichier ou dossier spécifique + Erreur de chargement du script d'une Partial View (fichier : %0%) + Erreur de chargement du userControl '%0%' + Erreur de chargement d'un customControl (Assembly: %0%, Type: '%1%') + Erreur de chargement d'un script du MacroEngine (fichier : %0%) + "Erreur de parsing d'un fichier XSLT : %0% + "Erreur de lecture d'un fichier XSLT : %0% + Veuillez entrer un titre + Veuillez choisir un type + Vous allez définir une taille d'image supérieure à sa taille d'origine. Êtes-vous certain(e) de vouloir continuer? Erreur dans le script Python - Le script Python n'a pas été sauvegardé, car il contient des erreur(s) + Le script Python n'a pas été sauvegardé car il contient des erreurs Noeud de départ supprimé, contactez votre administrateur - Séléctionnez du contenu avant de changer le style + Veuillez sélectionner du contenu avant de changer le style Aucun style actif disponible - Placez le curseur à gauche des deux cellules que vous voulez fusionner - Vous ne pouvez pas scinder une cellule que vous n'avez pas fusionné. + Veuillez placer le curseur à gauche des deux cellules que vous voulez fusionner + Vous ne pouvez pas scinder une cellule qui n'a pas été fusionnée. Erreur dans le code source XSLT - Le XSLT n'a pas été sauvegardé, car il contient des erreur(s) - Il y a une erreur de configuration du type de données de cette propriété. Vérifiez ce type de données. + Le XSLT n'a pas été sauvegardé car il contient des erreurs + Il y a une erreur de configuration du type de données utilisé pour cette propriété, veuillez vérifier le type de données. A propos @@ -310,8 +388,10 @@ Actions Ajouter Alias - Êtes-vous sûr ? - Bordure + Tout + Êtes-vous certain(e)? + Retour + Bord par Annuler Marge de cellule @@ -320,13 +400,13 @@ Fermer la fenêtre Commenter Confirmer - Contraindre les proportions + Conserver les proportions Continuer Copier Créer Base de données Date - Defaut + Défaut Supprimer Supprimé Suppression... @@ -336,26 +416,28 @@ Télécharger Editer Edité - Elements + Eléments Email Erreur Trouver Hauteur Aide - Icone + Icône Importer Marge intérieure Insérer Installer + Non valide Justifier - Langage - Layout - Chargement - Fermé + Langue + Mise en page + En cours de chargement + Bloqué Connexion Déconnexion Déconnexion Macro + Obligatoire Déplacer Plus Nom @@ -374,6 +456,7 @@ Propriétés Email de réception des données de formulaire Corbeille + Votre corbeille est vide Restant Renommer Renouveller @@ -381,12 +464,13 @@ Réessayer Permissions Rechercher + Désolé, nous ne pouvons pas trouver ce que vous recherchez Serveur Montrer Afficher la page à l'envoi Taille Trier - Submit + Envoyer Type Rechercher... Haut @@ -403,11 +487,45 @@ Oui Dossier Résultats de recherche - Reorder - I am done reordering + Réorganiser + J'ai fini de réorganiser + Prévisualiser + Modifier le mot de passe + vers + Liste + Sauvegarde... + actuel + Intégrer + sélectionné + + + Noir + Vert + Jaune + Orange + Bleu + Rouge + + + Ajouter un onglet + Ajouter une propriété + Ajouter un éditeur + Ajouter un modèle + Ajouter un noeud enfant + Ajouter un enfant + + Editer le type de données + + Parcourir les sections + + Raccourcis + afficher les raccourcis + + Passer à la vue en liste + Basculer vers l'autorisation comme racine - Background color + Couleur de fond Gras Couleur de texte Police @@ -417,131 +535,127 @@ Page - L'installeur n'a pas pu se connecter à la base de données. - Impossible de modifier le fichier web.config file. Modifiez s'il vous plait la "connection string" manuellement. - Votre base de données a été détectée et identifiée telle que - Configurtion de la base de données + Le programme d'installation ne parvient pas à se connecter à la base de données. + Impossible de sauvegarder le fichier web.config. Veuillez modifier la "connection string" manuellement. + Votre base de données a été détectée et est identifiée comme étant + Configuration de la base de données install pour installer la base de données Umbraco %0% + Appuyez sur le bouton installer pour installer la base de données Umbraco %0% ]]> - Suivant pour procéder.]]> - Base de données non trouvée ! Vérifiez les informations de la "connection string" dans le fichier web.config.

-

Pour poursuivre, éditez le fichier "web.config" (avec Visual Studio ou votre éditeur de texte favori), scrollez jusqu'en bas, ajoutez une "connection string" dans la ligne "umbracoDbDSN" et sauvegardez votre fichier.

+ Suivant pour poursuivre.]]> + Base de données non trouvée ! Veuillez vérifier les informations de la "connection string" dans le fichier web.config.

+

Pour poursuivre, veuillez éditer le fichier "web.config" (avec Visual Studio ou votre éditeur de texte favori), scroller jusqu'en bas, ajouter le "connection string" pour votre base de données dans la ligne avec la clé "umbracoDbDSN" et sauvegarder le fichier.

Cliquez sur le bouton Réessayer lorsque cela est fait.
Plus d'informations sur l'édition du fichier web.config ici.

]]>
- - Contactez votre administrateur système si nécessaire. - Si vous installez Umbraco sur votre ordinateur, vous aurez probablement besoin de consulter votre administrateur système.]]> + + Veuillez contacter votre fournisseur de services internet si nécessaire. + Si vous installez Umbraco sur un ordinateur ou un serveur local, vous aurez peut-être besoin de consulter votre administrateur système.]]> Appuyez sur le bouton Upgrader pour mettre à jour votre base de données vers Umbraco %0%

- Ne vous inquiétez pas : aucun contenu ne sera supprimé et tout continuera à fonctionner parfaitement ensuite ! + N'ayez pas d'inquiétude : aucun contenu ne sera supprimé et tout continuera à fonctionner parfaitement par après !

]]>
- Appuyez sur Suivant pour - poursuivre. ]]> + + Appuyez sur Suivant pour + poursuivre. ]]> + Suivant pour poursuivre la configuration]]> - Le mot de passe par défaut doit être changé !]]> - L'utilisateur par défaut a été désactivé ou n'a pas accès à Umbraco!

Aucune action n'est requise. Cliquez sur Suivant pour poursuivre.]]> - Le mot de passe par défaut a été modifié avec succès !

Aucune action n'est requise. Cliquez sur Suivant pour poursuivre.]]> - Le mot de passe a été changé ! - - Umbraco créer un utilisateur par défaut avec le login ('admin') et le mot de passe ('default'). - Il est important que ce mot de passe soit changé pour quelque-chose de sécurisé et unique. -

-

- Cette étape va vérifier le mot de passe par défaut et vérifier s'il est nécessaire de le changer. -

- ]]>
+ Le mot de passe par défaut doit être modifié !]]> + L'utilisateur par défaut a été désactivé ou n'a pas accès à Umbraco!

Aucune autre action n'est requise. Cliquez sur Suivant pour poursuivre.]]> + Le mot de passe par défaut a été modifié avec succès depuis l'installation!

Aucune autre action n'est requise. Cliquez sur Suivant pour poursuivre.]]> + Le mot de passe a été modifié ! + + ('admin') et le mot de passe ('default'). Il est important que ce mot de passe soit modifié en quelque-chose de sécurisé et unique.]]> Pour bien commencer, regardez nos vidéos d'introduction - En cliquant sur le bouton Suvant (ou en modifiant umbracoConfigurationStatus dans le fichier web.config), vous acceptez la licence de ce logiciel telle que spécifiée dans le champ ci-dessous. Remarque : cette distribution Umbraco consiste en deux licences différentes, la licence open source MIT pour le framework et la licence Umbraco freewarequi couvre l'UI. + En cliquant sur le bouton "Suivant" (ou en modifiant umbracoConfigurationStatus dans le fichier web.config), vous acceptez la licence de ce logiciel telle que spécifiée dans le champ ci-dessous. Veuillez noter que cette distribution Umbraco consiste en deux licences différentes, la licence open source MIT pour le framework et la licence Umbraco freeware qui couvre l'UI. Pas encore installé. - Fichiers et dossiers affectés - Plus d'informations sur la définition des permissions + Fichiers et dossiers concernés + Plus d'informations sur la configuration des permissions Vous devez donner à ASP.NET les droits de modification sur les fichiers/dossiers suivants - Vos permissions sont presques parfaites !

- Vous pouvez faire fonctionner Umbraco sans problèmes, mais vous ne serez pas en mesure d'installer des packages, ce qui est hautement recommandé pour tirer pleinement parti d'Umbraco.]]>
- Comment le résoudre + Vos configurations de permissions sont presque parfaites !

+ Vous pouvez faire fonctionner Umbraco sans problèmes, mais vous ne serez pas en mesure d'installer des packages, ce qui est hautement recommandé pour tirer pleinement profit d'Umbraco.]]>
+ Comment résoudre Cliquez ici pour lire la version texte - tutoriel vidéo sur la définition des permissions pour Umbraco, ou lisez la version texte.]]> - Vos permissions pourraient être problématiques ! + tutoriel vidéo sur la définition des permissions des répertoires pour Umbraco, ou lisez la version texte.]]> + Vos configurations de permissions pourraient poser problème !

- Vous pouvez faire fonctionner Umbraco sans problèmes, mais vous ne serez pas en mesure d'installer des packages, ce qui est hautement recommandé pour tirer pleinement parti d'Umbraco.]]>
- Vos permissions ne sont pas prêtes pour Umbraco ! + Vous pouvez faire fonctionner Umbraco sans problèmes, mais vous ne serez pas en mesure d'installer des packages, ce qui est hautement recommandé pour tirer pleinement profit d'Umbraco.]]> + Vos configurations de permissions ne sont pas prêtes pour Umbraco !

Pour faire fonctionner Umbraco, vous aurez besoin de mettre à jour les permissions sur les fichiers/dossiers.]]>
- Vos permissions sont parfaites !

- Vous êtes prêt à faire fonctionner Umbraco et installer des packages !]]>
+ Vos configurations de permissions sont parfaites !

+ Vous êtes prêt(e) à faire fonctionner Umbraco et à installer des packages !]]>
Résoudre un problème sur un dossier - Suivz ce lien pour plus d'informations sur ASP.NET et la création de dossiers + Suivez ce lien pour plus d'informations sur les problèmes avec ASP.NET et la création de dossiers Définir les permissions de dossier Je veux démarrer "from scratch" - + Apprenez comment) - Vous pouvez toujours choisir d'installer Runway plus tard. Pour cela allez dans la séction "Développeur" et séléctionnez "Packages". + Vous pouvez toujours choisir d'installer Runway plus tard. Pour cela, allez dans la section "Développeur" et sélectionnez "Packages". ]]> - Vous avez mis en oeuvre une plateforme Umbraco clean. Que voulez-vous faire ensuite ? + Vous venez de mettre en place une plateforme Umbraco toute nette. Que voulez-vous faire ensuite ? Runway est installé + Les fondations en place. Choisissez les modules que vous souhaitez installer par-dessus
Voici la liste des modules recommandés, cochez ceux que vous souhaitez installer, ou regardez la liste complète des modules ]]>
Recommandé uniquement pour les utilisateurs expérimentés Je veux commencer avec un site simple - + - "Runway" est un simple site fournissant des types de documents et modèles basiques. L'installeur peut mettre en oeuvre Runway automatiquement pour vous, - mais vous pouvez facilement léditer, lenrichir, ou le supprimer ensuite. Il n'est pas nécessaire et vous pouvez parfaitement utiliser Umbraco sans. Pour autant, - Runway offre un socle facile basé sur des bonnes pratiques pour vous permettre de commencer rapidement. - Si vous choisissez d'installer Runway, vous pouvez, de manière optionnelle, choisir des blocks appelés Runway Modules pour enrichir les pages du site. + "Runway" est un site simple qui fournit des types de documents et des modèles de base. L'installateur peut mettre en place Runway automatiquement pour vous, + mais vous pouvez facilement l'éditer, l'enrichir, ou le supprimer par la suite. Il n'est pas nécessaire, et vous pouvez parfaitement vous en passer pour utiliser Umbraco. Cela étant dit, + Runway offre une base facile, fondée sur des bonnes pratiques, pour vous permettre de commencer plus rapidement que jamais. + Si vous choisissez d'installer Runway, vous pouvez sélectionner en option des blocs de base, appelés Runway Modules, pour enrichir les pages de votre site.

Inclus avec Runway : Home page, Getting Started page, Installing Modules page.
Modules optionnels : Top Navigation, Sitemap, Contact, Gallery.
]]>
- Qu'est ce que Runway - Step 1/5 : Licence - Step 2/5 : Configuration base de données - Step 3/5 : Validation des permissions de fichiers - Step 4/5 : Sécurité Umbraco - Step 5/5 : Umbraco est prêt + Qu'est-ce que Runway + Etape 1/5 : Accepter la licence + Etape 2/5 : Configuration de la base de données + Etape 3/5 : Validation des permissions de fichiers + Etape 4/5 : Sécurité Umbraco + Etape 5/5 : Umbraco est prêt Merci d'avoir choisi Umbraco Parcourir votre nouveau site -Vous avez installé Runway, alors pourquoi pas jeter un oeil au look de votre nouveau site ?]]> - Aide et information -Obtenez de l'aide de notre award winning communauté, parcourez la documentation our regardez quelques vidéos sur "Comemnt construire un site simple", "Comment utiliser les packages" et un guide rapide sur la terminologie Umbraco]]> +Vous avez installé Runway, alors pourquoi ne pas jeter un oeil au look de votre nouveau site ?]]>
+ Aide et informations complémentaires +Obtenez de l'aide de notre "award winning" communauté, parcourez la documentation ou regardez quelques vidéos gratuites sur la manière de construire un site simple, d'utiliser les packages ainsi qu'un guide rapide sur la terminologie Umbraco]]> Umbraco %0% est installé et prêt à être utilisé - /web.config file et mettre à jour le paramètre AppSetting dans umbracoConfigurationStatus en bas de la valeur de '%0%'.]]> - démarrer maintenant en cliquant sur le bouton "Lancer Umbraco" ci-dessous.
-Si vous débutez sur Umbraco, vous pouvez trouver plein de ressources sur nos pages "Getting Started".]]>
+ fichier /web.config et mettre à jour le paramètre AppSetting umbracoConfigurationStatus situé en bas à la valeur '%0%'.]]> + démarrer instantanément en cliquant sur le bouton "Lancer Umbraco" ci-dessous.
+Si vous débutez avec Umbraco, vous pouvez trouver une foule de ressources dans nos pages "Getting Started".]]>
Lancer Umbraco -Pour gérer votre site, ouvrez simplement le backoffice Umbraco et commencez à ajouter du contenu, mettez à jour les templates et feuilles de styles ou ajoutez ds nouvelles fonctionnalités]]> +Pour gérer votre site, ouvrez simplement le backoffice Umbraco et commencez à ajouter du contenu, à mettre à jour les modèles d'affichage et feuilles de styles ou à ajouter de nouvelles fonctionnalités]]> La connexion à la base de données a échoué. Umbraco Version 3 Umbraco Version 4 Regarder - umbraco %0%, qu'il s'agisse d'une installation récente ou de la version 3.0 + Umbraco %0%, qu'il s'agisse d'une nouvelle installation ou d'une mise à jour à partir de la version 3.0

- Appuyez sur Press "suivant" pour commencer.]]>
+ Appuyez sur "suivant" pour commencer l'assistant.]]>
Code de la Culture Nom de la culture - Vous avez été inactif, la déconnexion automatique se fera dans + Vous avez été inactif et la déconnexion aura lieu automatiquement dans Renouvellez votre session maintenant pour sauvegarder votre travail @@ -554,7 +668,18 @@ Pour gérer votre site, ouvrez simplement le backoffice Umbraco et commencez à Joyeux samedi Connectez-vous ci-dessous La session a expiré - © 2001 - %0%
umbraco.com

]]>
+ © 2001 - %0%
Umbraco.com

]]>
+ Mot de passe oublié? + Un email contenant un lien pour ré-initialiser votre mot de passe sera envoyé à l'adresse spécifiée + Un email contenant les instructions de ré-initialisation de votre mot de passe sera envoyée à l'adresse spécifiée si elle correspond à nos informations. + Revenir au formulaire de connexion + Veuillez fournir un nouveau mot de passe + Votre mot de passe a été mis à jour + Le lien sur lequel vous avez cliqué est non valide ou a expiré. + Umbraco: Ré-initialiser le mot de passe + + Votre nom d'utilisateur pour vous connecter au back-office Umbraco est : %0%.

Cliquez ici pour ré-initialiser votre mot de passe, ou recopiez cet URL dans votre navigateur :

%1%

]]> +
Tableau de bord @@ -562,21 +687,21 @@ Pour gérer votre site, ouvrez simplement le backoffice Umbraco et commencez à Contenu - Choisissez une page ci-dessous... + Choisissez la page au-dessus... %0% a été copié dans %1% - Choisissez, ci-dessous, où le document %0% doit être copié + Choisissez ci-dessous l'endroit où le document %0% doit être copié %0% a été déplacé dans %1% - Choisissez, ci-dessous, où le document %0% doit être déplacé - a été choisi comme racine de votre contenu, cliquez sur 'ok' ci-dessous. - Aucun noeud choisi, choisissez s'il vous plait un noeud dans la liste ci-dessus avant de cliquer sur 'ok'. - Le noeud actuel n'est pas autorisé dans le noeud choisi à cause de son type + Choisissez ci-dessous l'endroit où le document %0% doit être déplacé + a été choisi comme racine de votre nouveau contenu, cliquez sur 'ok' ci-dessous. + Aucun noeud n'a encore été choisi, veuillez choisir un noeud dans la liste ci-dessus avant de cliquer sur 'ok'. + Le noeud actuel n'est pas autorisé sous le noeud choisi à cause de son type Le noeud actuel ne peut pas être déplacé dans une de ses propres sous-pages Le noeud actuel ne peut pas exister à la racine - L'action n'est pas autorisée car vous n'avez pas les droits sur un ou plus des noeuds enfants. - Relier les items copiés à l'original + L'action n'est pas autorisée car vous n'avez pas les droits suffisants sur un ou plusieurs noeuds enfants. + Relier les éléments copiés à l'original - Editer vos notifications pour %0% + Editez vos notifications pour %0% - Hi %0%

+ + Hello %0%

Ceci est un email automatique pour vous informer que la tâche '%1%' a été executée sur la page '%2%' @@ -598,11 +724,11 @@ Pour gérer votre site, ouvrez simplement le backoffice Umbraco et commencez à

-

Update summary:

+

Résumé de la mise à jour :

%6%
@@ -610,71 +736,71 @@ Pour gérer votre site, ouvrez simplement le backoffice Umbraco et commencez à

Bonne journée !

- Le Robot Umbraco vous salue + Avec les salutations du Robot Umbraco

]]>
La notification [%0%] à propos de %1% a été executée sur %2% Notifications - localisez le package. Les packages Umbraco ont généralement une extension .umb ou .zip. + Choisissez un package sur votre ordinateur en cliquant sur le bouton Parcourir
+ et localisez le package. Les packages Umbraco ont généralement une extension ".umb" ou ".zip". ]]>
Auteur - Demon + Démo Documentation Meta data du package Nom du package - Le package ne contient aucun éléments -
- Vous pouvez supprimer tranquillement le package de votre installation en cliquant sur "Désinstaller" ci-dessous.]]>
- Aucune mises à jour disponibles - Options de package + Le package ne contient aucun élément +
+ Vous pouvez supprimer tranquillement ce package de votre installation en cliquant sur "Désinstaller le package" ci-dessous.]]>
+ Aucune mise à jour disponible + Options du package Package readme - Package repository + Repository des packages Confirmation de désinstallation - Package was uninstalled + Le package a été désinstallé Le package a été désinstallé avec succès - Désintaller le package - - Remarque : tous les documents, media etc dépendants des éléments que vous supprimerez, arrêteront de fonctionner, ce qui peut provoquer une instabilité du système, - désinstallez avec prudence. En cas de doute, contactez l'auteur du package.]]> + Désinstaller le package + + Remarque : tous les documents, media etc. dépendant des éléments que vous supprimez vont cesser de fonctionner, ce qui peut provoquer une instabilité du système, + désinstallez donc avec prudence. En cas de doute, contactez l'auteur du package.]]> Télécharger la mise à jour depuis le repository Mettre à jour le package Instructions de mise à jour - Il y a une mise à jour disponible pour ce package. Vous pouvez la télécharger directement depuis le repository. - Version de package - Historique des version de package + Il y a une mise à jour disponible pour ce package. Vous pouvez la télécharger directement depuis le repository des packages Umbraco. + Version du package + Historique des versions du package Voir le site internet du package Coller en conservant le formatage (non recommandé) - Le texte que vous tentez de coller contient des caractères spéciaux ou de formatage. Cela peut être dû à une copie d'un texte de Microsoft Word. Umbraco peut supprimer les caractères spéciaux et le formatage automatiquement, de manière à ce que le texte collé soit plus utilisable pour le Web. - Coller en tant que texte brut sans formatage - Coller, mais supprimer le formatage (recommendé) + Le texte que vous tentez de coller contient des caractères spéciaux ou du formatage. Cela peut être dû à une copie d'un texte depuis Microsoft Word. Umbraco peut supprimer automatiquement les caractères spéciaux et le formatage, de manière à ce que le texte collé convienne mieux pour le Web. + Coller en tant que texte brut sans aucun formatage + Coller, mais supprimer le formatage (recommandé) Protection basée sur les rôles via les groupes de membres Umbraco.]]> - l'authentification basée sur les rôles.]]> + l'authentification basée sur les rôles.]]> Page d'erreur - Utilisé quand les gens sont connectés, mais n'ont pas accès + Utilisé pour les personnes connectées, mais qui n'ont pas accès Choisissez comment restreindre l'accès à cette page %0% est maintenant protégée Protection supprimée de %0% Page de connexion - Choisissez la page qui a le formulaire de login + Choisissez la page qui contient le formulaire de connexion Supprimer la protection - Choisissez la page qui contient le formulaire de login et les messages d'erreur - Piochez les roles qui ont accès à cette page - Définissez l'identifiant et mot de passe pour cette page + Choisissez les pages qui contiennent le formulaire de connexion et les messages d'erreur + Choisissez les rôles qui ont accès à cette page + Définissez l'identifiant et le mot de passe pour cette page Protection utilisateur unique - Si vous souhaitez simplement mettre en place une protection par identifiant et mot de passe + Si vous souhaitez mettre en place une protection simple utilisant un identifiant et un mot de passe uniques @@ -682,44 +808,47 @@ Pour gérer votre site, ouvrez simplement le backoffice Umbraco et commencez à %0% n'a pas pu être publié car cet élément est programmé pour être publié bientôt. ]]> + - Inclure les pages enfants non publiées + Inclure les pages enfant non publiées Publication en cours - veuillez patienter... - %0% sur %1% des pages ont été publiées... + %0% pages sur %1% ont été publiées... %0% a été publié %0% et ses pages enfants ont été publiées - Publier %0% et ses pages enfants - ok pour publier %0% et le rendre ainsi accessible publiquement.

- Vous pouvez publier cette page et ses sous pages en cochant publier tous les enfants ci-dessous. + Publier %0% et toutes ses pages enfant + Publier pour publier %0% et la rendre ainsi accessible publiquement.

+ Vous pouvez publier cette page et toutes ses sous-pages en cochant Inclure les pages enfant non pubiées ci-dessous. ]]>
- Vous n'avez configuré aucune couleurs approuvées + Vous n'avez configuré aucune couleur approuvée - Ajouter un lien externe - Ajouter un lien interne - Ajouter + introduire un lien externe + choisir une page interne Légende - Page interne - URL - Descendre - Monter + Lien Ouvrir dans une nouvelle fenêtre - Supprimer le lien + introduisez la légende à afficher + Introduiser le lien + + + Réinitialiser Version actuelle - Le texte en Rouge signifit qu'il a été supprimé de la version choisie, vert signifie ajouté]]> - Le document est passé à une version antérieure + Le texte en Rouge signifie qu'il a été supprimé de la version choisie, vert signifie ajouté]]> + Le document a été restauré à une version antérieure Ceci affiche la version choisie en tant que HTML, si vous souhaitez voir les différences entre les deux versions en même temps, utilisez la vue différentielle Revenir à Choisissez une version @@ -737,16 +866,24 @@ Pour gérer votre site, ouvrez simplement le backoffice Umbraco et commencez à Medias Membres Newsletters - Paramètres + Configuration Statistiques Traduction Utilisateurs Aide + Formulaires + Analytics + + + aller à + Rubriques d'aided pour + Chapitres vidéo pour + Les meilleurs tutoriels vidéo Umbraco - Template par défaut + Modèle par défaut Clé de dictionnaire - Pour importer un type de document, trouvez le fichier ".udt" sur votre ordinateur en cliquant sur le bouton "Parcourir" et cliquez sur "Importer" (une confirmation vous sera demandé à l'écran d'après) + Pour importer un type de document, trouvez le fichier ".udt" sur votre ordinateur en cliquant sur le bouton "Parcourir" et cliquez sur "Importer" (une confirmation vous sera demandée à l'écran suivant) Titre du nouvel onglet Type de noeud Type @@ -756,22 +893,29 @@ Pour gérer votre site, ouvrez simplement le backoffice Umbraco et commencez à Onglet Titre de l'onglet Onglets - Type de contenu master activé + Type de contenu de base activé Ce type de contenu utilise - en tant que type de contenu master, les onglets du type de contenu master ne sont pas affichés et peuvent seulement être modifiés dans le type de contenu master lui-même. - Aucune propriétés définies dans cet onglet. Cliquez sur le lien "Ajouter une nouvelle propriété" en haut pour créer une nouvelle propriété. - Type de contenu parent - Créer le template correspondant - + en tant que type de contenu de base. Les onglets du type de contenu de base ne sont pas affichés et peuvent seulement être modifiés à partir du type de contenu de base lui-même. + Aucune propriété définie dans cet onglet. Cliquez sur le lien "Ajouter une nouvelle propriété" en-haut pour créer une nouvelle propriété. + Type de contenu de base + Créer le modèle correspondant + Ajouter une icône + - Sort order - Creation date + Ordre de tri + Date de création Tri achevé. - Faites glisser les différents éléments ci-dessous vers le haut ou le bas pour définir comment ils doivent être triés. Ou cliquez sur les entêtes de colonne pour trier la collection complète d'éléments + Faites glisser les différents éléments vers le haut ou vers le bas pour définir la manière dont ils doivent être organisés. Ou cliquez sur les entêtes de colonnes pour trier la collection complète d'éléments
Ne fermez pas cette fenêtre durant le tri.]]>
- La publication a été annulée par un extension tierce. + Validation + Les erreurs de validation doivent être corrigées avant de pouvoir sauvegarder l'élément + Echec + Permissions utilisateur insuffisantes, l'opération n'a pas pu être complétée + Annulation + L'opération a été annulée par une extension tierce + La publication a été annulée par une extension tierce. Le type de propriété existe déjà Type de propriété créé Type de données : %1%]]> @@ -779,33 +923,35 @@ Pour gérer votre site, ouvrez simplement le backoffice Umbraco et commencez à Type de documet sauvegardé Onglet créé Onglet supprimé - Onglet d'ID : %0% supprimé + Onglet avec l'ID : %0% supprimé Feuille de style non sauvegardée Feuille de style sauvegardée Feuille de style sauvegardée sans erreurs - Type de données sauvegardée - Element de dictionnaire sauvegardé + Type de données sauvegardé + Elément de dictionnaire sauvegardé La publication a échoué car la page parent n'est pas publiée Contenu publié et visible sur le site - Content sauvegardé - N'oubliez pas de publier pour rendre les changements visibles + Contenu sauvegardé + N'oubliez pas de publier pour rendre les modifications visibles Envoyer pour approbation - Les changements ont été envoyés pour approbation + Les modifications ont été envoyées pour approbation Media sauvegardé Media sauvegardé sans erreurs Membre sauvegardé - Propriété de feuille de style sauvegardé - Feuille de style sauvegardé - Template sauvegardé - Erreur lors de la sauvegarde de l'utilisateur + Propriété de feuille de style sauvegardée + Feuille de style sauvegardée + Modèle sauvegardé + Erreur lors de la sauvegarde de l'utilisateur (consultez les logs) Utilisateur sauvegardé Type d'utilisateur sauvegardé Fichier non sauvegardé Le fichier n'a pas pu être sauvegardé. Vérifiez les permissions de fichier. Fichier sauvegardé Fichier sauvegardé sans erreurs - Langage sauvegardé + Langue sauvegardée + Type de média sauvegardé + Type de membre sauvegardé Le script Python n'a pas été sauvegardé Le script Python n'a pas été sauvegardé à cause d'erreurs Le script Python a été sauvegardé @@ -818,30 +964,35 @@ Pour gérer votre site, ouvrez simplement le backoffice Umbraco et commencez à Le XSLT contenait une erreur Le XSLT n'a pas pu être sauvegardé, vérifiez les permissions de fichier Le XSLT a été sauvegardé - Aucune erreurs dans le XSLT + Aucune erreur dans le XSLT Contenu publié Vue partielle sauvegardée Vue partielle sauvegardée sans erreurs ! Vue partielle non sauvegardée Une erreur est survenue lors de la sauvegarde du fichier. - + Vue script sauvegardée + Vue script sauvegardée sans erreur ! + Vue script non sauvegardée + Une erreur est survenue lors de la sauvegarde du fichier. + Une erreur est survenue lors de la sauvegarde du fichier. + - Utilise la synthaxe CSS. Ex : h1, .redHeader, .blueTex + Utilise la syntaxe CSS. Ex : h1, .redHeader, .blueTex Editer la feuille de style Editer la propriété de feuille de style - Nommer pour identifier la propriété dans le Rich Text Editor + Donner un nom pour identifier la propriété dans le Rich Text Editor Prévisualiser Styles - Editer le modèle template + Editer le modèle Insérer une zone de contenu Insérer un placeholder de zone de contenu Insérer un élément de dictionnaire - Insert Macro - Insert umbraco page field - Modèle master - Guide rapide aux tags des modèles Umbraco + Insérer une Macro + Insérer un champ de la page Umbraco + Modèle de base + Guide rapide concernant les tags des modèles Umbraco Modèle @@ -849,40 +1000,113 @@ Pour gérer votre site, ouvrez simplement le backoffice Umbraco et commencez à Choisissez une mise en page Ajouter une ligne Ajouter du contenu - Contenu goutte + Supprimer le contenu Paramètres appliqués - Ce contenu est pas autorisée ici - Ce contenu est permis ici + Ce contenu n'est pas autorisé ici + Ce contenu est autorisé ici Cliquez pour intégrer - Cliquez pour insérer l'image + Cliquez pour insérer une image Légende de l'image... - Ecrire ici... + Ecrivez ici... - Layouts Grid - Layouts sont la superficie totale de travail pour l'éditeur de grille, en général, vous avez seulement besoin d'une ou deux configurations différentes - Ajouter Grid Layout - Ajustez la mise en page en définissant la largeur des colonnes et ajouter des sections supplémentaires - Configurations des lignes - Les lignes sont des cellules prédéfinies disposées horizontalement - Ajouter une configuration de la ligne - Ajustez la ligne en réglant la largeur des cellules et en ajoutant des cellules supplémentaires + Mises en pages de la Grid + Les mises en pages représentent la surface de travail globale pour l'éditeur de grille, en général, vous n'avez seulement besoin que d'une ou deux mises en pages différentes + Ajouter une mise en page de grille + Ajustez la mise en page en définissant la largeur des colonnes et en ajoutant des sections supplémentaires + Configurations des rangées + Les rangées sont des cellules prédéfinies disposées horizontalement + Ajouter une configuration de rangée + Ajustez la rangée en réglant la largeur des cellules et en ajoutant des cellules supplémentaires Colonnes Nombre total combiné de colonnes dans la configuration de la grille Paramètres - Configurer quels paramètres éditeurs peuvent changer + Configurez les paramètres qui peuvent être modifiés par les éditeurs - Modes - Configurer ce style éditeurs peuvent changer + + Styles + Configurez les effets de style qui peuvent être modifiés par les éditeurs - Les réglages seulement économiser si la configuration du json saisi est valide + Les paramètres ne seront sauvegardés que si la configuration json saisie est valide Autoriser tous les éditeurs - Autoriser toutes les configurations de lignes + Autoriser toutes les configurations de rangées + Configurer comme défaut + Choisir en plus + Choisir le défaut + ont été ajoutés + + + Compositions + Vous n'avez pas ajouté d'onglet + Ajouter un nouvel onglet + Ajouter un autre onglet + Hérité de + Ajouter une propriété + Label requis + + Activer la vue en liste + Configure l'élément de contenu de manière à afficher ses éléments enfants sous forme d'une liste que l'on peut trier et filtrer, les enfants ne seront pas affichés dans l'arborescence + + Modèles autorisés + Sélectionnez les modèles que les éditeurs sont autorisés à utiliser pour du contenu de ce type. + Autorisé comme racine + Autorisez les éditeurs à créer du contenu de ce type à la racine de l'arborescence de contenu. + Oui - autoriser du contenu de ce type à la racine + + Types de noeuds enfants autorisés + Autorisez la création de contenu des types spécifiés sous le contenu de ce type-ci + + Choisissez les noeuds enfants + Hériter des onglets et propriétés d'un type de document existant. De nouveaux onglets seront ajoutés au type de document actuel, ou fusionnés s'il existe un onglet avec un nom sililaire. + Ce type de contenu est utilisé dans une composition, et ne peut donc pas être lui-même un composé. + Il n'y a pas de type de contenu disponible à utiliser dans une composition. + + Editeurs disponibles + Réutiliser + Configuration de l'éditeur + + Configuration + + Oui, supprimer + + a été déplacé en-dessous + a été copié en-dessous + Sélectionnez le répertoire à déplacer + Sélectionnez le répertoire à copier + dans l'arborescence ci-dessous + + Tous les types de document + Tous les documents + Tous les éléments media + + utilisant ce type de document seront supprimés définitivement, veuillez confirmer que vous souhaitez les supprimer également. + utilisant ce type de media seront supprimés définitivement, veuillez confirmer que vous souhaitez les supprimer également. + utilisant ce type de membre seront supprimés définitivement, veuillez confirmer que vous souhaitez les supprimer également + + et tous les documents utilisant ce type + et tous les éléments media utilisant ce type + et tous les membres utilisant ce type + + utilisant cet éditeur seront mis à jour avec la nouvelle configuration + + Le membre peut éditer + Afficher dans le profil du membre + l'onglet n'a pas d'ordonnancement + + + + Création des modèles + ceci peut prendre un certain temps, ne vous inquiétez pas + Les modèles ont été générés + Les modèles n'ont pas pu être générés + La génération des modèles a échoué, veuillez consulter les erreurs dans le log Umbraco + + Champ alternatif Texte alternatif @@ -896,67 +1120,68 @@ Pour gérer votre site, ouvrez simplement le backoffice Umbraco et commencez à Formater comme une date Encoder en HTML Remplacera les caractères spéciaux par leur équivalent HTML. - Sera insérer après la valeur du champ - Sera inséré après la valeur du champ + Sera inséré après la valeur du champ + Sera inséré avant la valeur du champ Minuscules Aucun - Inserer après le champ - Inserer après le champ - Recursif - Supprimer les balises paragraphes - Supprimera tous les &lt;P&gt; + Insérer après le champ + Insérer avant le champ + Récursif + Supprimer les balises de paragraphes + Supprimera toute balise &lt;P&gt; au début et à la fin du texte Champs standards Majuscules - Encode en URL - Formatera les caractères spéciaux de manière à ce qu'ils soient utilisés dans une URL - Sera seulement utilisé quand toutes les valeurs ci-dessous seront vides - Ce champ sera utilisé seulement si le champ primaire est vide - Yes, with time. Separator: + Encode pour URL + Formatera les caractères spéciaux de manière à ce qu'ils soient reconnus dans une URL + Sera seulement utilisé si toutes les valeurs des champs ci-dessus sont vides + Ce champ sera utilisé seulement si le champ initial est vide + Oui, avec l'heure. Séparateur: Tâches qui vous sont assignées - vous sont assignées. Pour voir une vue détaillée incluant les commentaires, cliquez sur "Details" ou juste le nom de la page. - Vous pouvez télécharger la format au format XML en cliquant sur le lien "Télécharger XML".
- Pour terminer une tâche de traduction, allez sur Details, puis cliquer sur le bouton "Terminer tâche". + vous sont assignées. Pour voir un aperçu détaillé incluant les commentaires, cliquez sur "Détails" ou juste sur le nom de la page. + Vous pouvez aussi télécharger la page au format XML en cliquant sur le lien "Télécharger XML".
+ Pour clôturer une tâche de traduction, allez sur Détails, puis cliquez sur le bouton "Terminer la tâche". ]]>
- Terminer tâche + Terminer la tâche Détails - Télécharger toutes les traductions au format XML + Télécharger toutes les tâches de traductions au format XML Télécharger XML Télécharger la DTD XML Champs Inclure les pages enfants [%0%] tâches de traductions pour %1% - Aucun utilisateurs traducteurs trouvés. Vous devez créer un utilisateur traducteur avant d'envoyer votre contenu pour traduction - Tâches que vous avez créé - créées par vous. Pour voir une vue détaillée incluant les commentaires, - cliquez sur "Détails" ou juste le nom de la page. Vous pouvez aussi télécharger la page au format XML en cliquant sur le lien "Télécharger XML". - Pour terminer une tâche de traduction, allez sur Details, puis cliquer sur le bouton "Terminer tâche". + Aucun utilisateur traducteur trouvé. Veuillez créer un utilisateur traducteur avant d'envoyer du contenu pour traduction + Tâches que vous avez créées + que vous avez créées. Pour voir un aperçu détaillé incluant les commentaires, + cliquez sur "Détails" ou juste sur le nom de la page. Vous pouvez aussi télécharger la page au format XML en cliquant sur le lien "Télécharger XML". + Pour clôturer une tâche de traduction, allez sur Détails, puis cliquez sur le bouton "Terminer tâche". ]]> - La page '%0%' a été envoyé pour traduction + La page '%0%' a été envoyée pour traduction + Veuillez choisir la langue dans laquelle le contenu doit être traduit Envoyer la page '%0%' pour traduction Assignée par - Tâches ouvertures + Tâches ouvertes Nombre total de mots Traduire en Traduction complétée. - Vous pouvez prévisualiser les pages que vous avez traduites, en cliquant ci-dessous. Si la page originale est trouvée, vous aurez la comparaison entre les deux pages. - Traductio échouée, il semble que le fichier XML soit corrompu + Vous pouvez prévisualiser les pages que vous avez traduites en cliquant ci-dessous. Si la page originale est trouvée, vous verrez une comparaison entre les deux pages. + Traduction échouée, il se pourrait que fichier XML soit corrompu Options de traduction Traducteur Uploader le fichier de traduction XML @@ -965,20 +1190,21 @@ Pour gérer votre site, ouvrez simplement le backoffice Umbraco et commencez à Navigateur de cache Corbeille Packages créés - Typesde données + Types de données Dictionnaire Packages installés - Installer un skin + Installer une skin Installer un starter kit - Langages + Langues Installer un package local Macros - Types de médias + Types de média Membres Groupes de membres Rôles Types de membres Types de documents + Types de relations Packages Packages Fichiers Python @@ -990,53 +1216,166 @@ Pour gérer votre site, ouvrez simplement le backoffice Umbraco et commencez à Feuilles de style Modèles Fichiers XSLT + Analytique - Nouvelle mise à jour prête - %0% est prêt, cliquez ici pour télécharger + Nouvelle mise à jour disponible + %0% est disponible, cliquez ici pour télécharger Aucune connexion au serveur - Erreur lors de la recherche de mises à jour. Vérifiez la stack trace pour obtenir plus d'informations sur l'erreur. + Erreur lors de la recherche de mises à jour. Veuillez vérifier le stack trace pour obtenir plus d'informations sur l'erreur. Administrateur Champ catégorie Changer le mot de passe - Changez votre mot de passe + Nouveau mot de passe Confirmez votre nouveau mot de passe - Vous pouvez changer votre mot de passe d'accès à Umbraco en remplissant le formulaire ci-dessous puis en cliquant sur le bouton "Changer le mot de passe" + Vous pouvez changer votre mot de passe d'accès au Back Office Umbraco en remplissant le formulaire ci-dessous puis en cliquant sur le bouton "Changer le mot de passe" Canal de contenu Champ description Désactiver l'utilisateur Type de document Editeur Champ extrait - Langage + Langue Identifiant - Noeud de départ dans la librarie de médias + Noeud de départ dans la librarie de média Sections Désactiver l'accès Umbraco + Ancien mot de passe Mot de passe Réinitialiser le mot de passe - Your password has been changed! - Confirmez s'il vous plait votre nouveau mot de passe - Entrez votre nouveau mot de passe + Votre mot de passe a été modifié! + Veuillez confirmer votre nouveau mot de passe + Introduisez votre nouveau mot de passe Votre nouveau mot de passe ne peut être vide ! Mot de passe actuel Mot de passe actuel invalide - Il y avait une différence entre le nouveau mot de passe et le mot de passe confirmé. Veuillez réessayer - Le mot de passe confirmé ne match pas le nouveau mot de passe saisi - Remplacer les permissions des noeuds enfants - Vous modifiez actuellement les permissions pour les pages : - Choisissez les pages pour lesquelles modifier les permissions + Il y a une différence entre le nouveau mot de passe et le mot de passe confirmé. Veuillez réessayer. + Le mot de passe confirmé ne correspond pas au nouveau mot de passe saisi! + Remplacer les permissions sur les noeuds enfants + Vous êtes en train de modifiez les permissions pour les pages : + Choisissez les pages dont les permissions doivent être modifiées Rechercher tous les enfants Noeud de départ du contenu Nom d'utilisateur Permissions utilisateur Type d'utilisateur - Types d'utilisateur + Types d'utilisateurs Rédacteur + Traducteur + Modifier Votre profil Votre historique récent La session expire dans - + + Validation + Valider comme email + Valider comme nombre + Valider comme Url + ...ou introduisez une validation spécifique + Champ obligatoire + + + + La valeur est égale à la valeur recommandée : '%0%'. + La valeur du XPath '%2%' a été fixée à '%1%' dans le fichier de configuration '%3%'. + La valeur attendue pour '%2%' dans le fichier de configuration '%3%' est '%1%', mais la valeur trouvée est '%0%'. + La valeur inattendue '%0%' a été trouvée pour '%2%' dans le fichier de configuration '%3%'. + + + Custom errors est fixé à la valeur '%0%'. + Custom errors est pour la moment fixé à la valeur '%0%'. Il est recommandé de le fixer la valeur à '%1%' avant la mise en ligne. + Custom errors a été rectifié avec succès à la valeur '%0%'. + + MacroErrors est fixé à la valeur '%0%'. + MacroErrors est fixé à la valeur '%0%', ce qui empêchera certaines ou même toutes les pages de votre site de se charger complètement en cas d'erreur dans les macros. La rectification de ceci fixera la valeur à '%1%'. + MacroErrors est maintenant fixé à la valeur '%0%'. + + + Try Skip IIS Custom Errors est fixé à la valeur '%0%' et vous utilisez IIS version '%1%'. + Try Skip IIS Custom Errors est actuellement fixé à '%0%'. Il est recommandé de fixer la valeur à '%1%' pour votre version IIS (%2%). + Try Skip IIS Custom Errors a été rectifié avec succès à la valeur '%0%'. + + + Le fichier n'existe pas : '%0%'. + '%0%' dans le fichier config '%1%'.]]> + Une erreur est survenue, consultez le log pour voir l'erreur complète : %0%. + + Total XML : %0%, Total : %1% + Total XML : %0%, Total : %1% + Total XML : %0%, Total publié : %1% + + Erreur de validation du certificat : '%0%' + Erreur en essayant de contacter l'URL %0% - '%1%' + Vous êtes actuellement %0% à voir le site via le schéma HTTPS. + La valeur appSetting 'umbracoUseSSL' est fixée à 'false' dans votre fichier web.config. Une fois que vous donnerez accès à ce site en utilisant le schéma HTTPS, cette valeur devra être mise à 'true'. + La valeur appSetting 'umbracoUseSSL' est fixée à '%0%' dans votre fichier web.config, vos cookies sont %1% marqués comme étant sécurisés. + Impossible de mettre à jour la configuration 'umbracoUseSSL' dans votre fichier web.config. Erreur : %0% + + + Activer HTTPS + Fixe la configuration 'umbracoSSL' à 'true' dans la section appSettings du fichier web.config. + La configuration appSetting 'umbracoUseSSL' est maintenant fixée à 'true' dans votre fichier web.config, vos cookies seront marqués comme étant sécurisés. + + Corriger + Impossible de corriger une vérification avec un type de comparaison 'ShouldNotEqual'. + Impossible de corriger une vérification avec un type de comparaison 'ShouldEqual' avec une valeur spécifiée. + La valeur de correction n'est pas fournie. + + Le mode de compilation Debug est désactivé. + Le mode de compilation Debug a été désactivé avec succès. + Le mode de compilation Debug est actuellement activé. Il est recommandé de désactiver ce paramètre avant la mise en ligne. + + Le mode tracing est désactivé. + Le mode tracing est actuellement activé. Il est recommandé de désactiver cette configuration avant la mise en ligne. + Le mode tracing a été désactivé avec succès. + + Tous les répertoires ont les configurations de permissions adéquates. + + %0%.]]> + %0%. Aucune action n'est requise s'il n'y a pas de nécessité d'y écrire.]]> + + Tous les fichiers ont les configurations de permissions adéquates. + + %0%.]]> + %0%. Aucune action n'est requise s'il n'y a pas de nécessité d'y écrire.]]> + + X-Frame-Options, utilisé pour contrôler si un site peut être intégré dans un autre via IFRAME, a été trouvé.]]> + X-Frame-Options , utilisé pour contrôler si un site peut être intégré dans un autre via IFRAME, n'a pas été trouvé.]]> + Configurez le Header dans le fichier Config + Ajoute une valeur dans la section httpProtocol/customHeaders du fichier web.config afin d'éviter que le site ne soit intégré dans d'autres sites via IFRAME. + Une configuration générant un header qui empêche l'intégration du site par d'autres sites via IFRAME a été ajoutée à votre fichier web.config. + Impossible de modifier le fichier web.config. Erreur : %0% + + + %0%.]]> + Aucun header révélant des informations à propos de la technologie du site web n'a été trouvé. + + La section system.net/mailsettings n'a pas pu être trouvée dans le fichier Web.config. + Dans la section system.net/mailsettings du fichier Web.config, le "host" n'est pas configuré. + La configuration SMTP est correcte et le service fonctionne comme prévu. + Le serveur SMTP configuré avec le host '%0%' et le port '%1%' n'a pas pu être contacté. Veuillez vérifier et vous assurer que la configuration SMTP est correcte dans la section system.net/mailsettings du fichier Web.config. + + %0%.]]> + %0%.]]> + From 8ee98ebdf9a944f26dd1e00a32a099e784e48ec2 Mon Sep 17 00:00:00 2001 From: michael Date: Fri, 9 Sep 2016 13:37:24 +0200 Subject: [PATCH 09/48] Review and add translation labels to language file fr.xml (sync with latest en version) --- src/Umbraco.Web.UI/umbraco/config/lang/fr.xml | 23 +++++++++++++++++++ 1 file changed, 23 insertions(+) diff --git a/src/Umbraco.Web.UI/umbraco/config/lang/fr.xml b/src/Umbraco.Web.UI/umbraco/config/lang/fr.xml index 2e5ee51bac..7a050d603a 100644 --- a/src/Umbraco.Web.UI/umbraco/config/lang/fr.xml +++ b/src/Umbraco.Web.UI/umbraco/config/lang/fr.xml @@ -209,6 +209,29 @@ Vous avez des changements en cours Etes-vous certain(e) de vouloir quitter cette page? - vous avez des changements en cours + + Terminé + %0% élément supprimé + %0% éléments supprimés + %0% élément sur %1% supprimé + %0% éléments sur %1% supprimés + %0% élément publié + %0% éléments publiés + %0% élément sur %1% publié + %0% éléments sur %1% publiés + %0% élément dépublié + %0% éléments dépubliés + %0% élément sur %1% dépublié + %0% éléments sur %1% dépubliés + %0% élément déplacé + %0% éléments déplacés + %0% élément sur %1% déplacé + %0% éléments sur %1% déplacés + %0% élément copié + %0% éléments copiés + %0% élément sur %1% copié + %0% éléments sur %1% copiés + Nom Gérer les noms d'hôtes From 6407c249779291d4d295443a56ee73d823f93f7e Mon Sep 17 00:00:00 2001 From: Michael Latouche Date: Sat, 10 Sep 2016 01:51:45 +0200 Subject: [PATCH 10/48] Update fr.xml --- src/Umbraco.Web.UI/umbraco/config/lang/fr.xml | 28 ------------------- 1 file changed, 28 deletions(-) diff --git a/src/Umbraco.Web.UI/umbraco/config/lang/fr.xml b/src/Umbraco.Web.UI/umbraco/config/lang/fr.xml index 8a75e4a7e0..7a050d603a 100644 --- a/src/Umbraco.Web.UI/umbraco/config/lang/fr.xml +++ b/src/Umbraco.Web.UI/umbraco/config/lang/fr.xml @@ -232,34 +232,6 @@ %0% élément sur %1% copié %0% éléments sur %1% copiés - - Done - - Deleted %0% item - Deleted %0% items - Deleted %0% out of %1% item - Deleted %0% out of %1% items - - Published %0% item - Published %0% items - Published %0% out of %1% item - Published %0% out of %1% items - - Unpublished %0% item - Unpublished %0% items - Unpublished %0% out of %1% item - Unpublished %0% out of %1% items - - Moved %0% item - Moved %0% items - Moved %0% out of %1% item - Moved %0% out of %1% items - - Copied %0% item - Copied %0% items - Copied %0% out of %1% item - Copied %0% out of %1% items - Nom Gérer les noms d'hôtes From 2d0725fbade7a4d13ff1778547c545762373bc58 Mon Sep 17 00:00:00 2001 From: michael Date: Thu, 22 Sep 2016 12:15:13 +0200 Subject: [PATCH 11/48] update with latest labels from en_us.xml --- src/Umbraco.Web.UI/umbraco/config/lang/fr.xml | 45 +++++++++++++++++-- 1 file changed, 41 insertions(+), 4 deletions(-) diff --git a/src/Umbraco.Web.UI/umbraco/config/lang/fr.xml b/src/Umbraco.Web.UI/umbraco/config/lang/fr.xml index 7a050d603a..40e18ce298 100644 --- a/src/Umbraco.Web.UI/umbraco/config/lang/fr.xml +++ b/src/Umbraco.Web.UI/umbraco/config/lang/fr.xml @@ -149,6 +149,7 @@ Propriétés Ce document est publié mais n'est pas visible car son parent '%0%' n'est pas publié Oups : ce document est publié mais n'est pas présent dans le cache (erreur interne Umbraco) + Oups: impossible d'obtenir cet url (erreur interne - voir fichier log) Oups: ce document est publié mais son url entrerait en collision avec le contenu %0% Publier Statut de publication @@ -170,7 +171,9 @@ Pas membre du/des groupe(s) Eléments enfants Cible - + Ceci se traduit par l'heure suivante sur le serveur : + Qu'est-ce que cela signifie?]]> + Cliquez pour télécharger Faites glisser vos fichier ici... @@ -211,22 +214,27 @@ Terminé + %0% élément supprimé %0% éléments supprimés %0% élément sur %1% supprimé %0% éléments sur %1% supprimés + %0% élément publié %0% éléments publiés %0% élément sur %1% publié %0% éléments sur %1% publiés + %0% élément dépublié %0% éléments dépubliés %0% élément sur %1% dépublié %0% éléments sur %1% dépubliés + %0% élément déplacé %0% éléments déplacés %0% élément sur %1% déplacé %0% éléments sur %1% déplacés + %0% élément copié %0% éléments copiés %0% élément sur %1% copié @@ -452,6 +460,7 @@ Installer Non valide Justifier + Libellé Langue Mise en page En cours de chargement @@ -592,7 +601,8 @@ Le mot de passe par défaut a été modifié avec succès depuis l'installation!

Aucune autre action n'est requise. Cliquez sur Suivant pour poursuivre.]]> Le mot de passe a été modifié ! - ('admin') et le mot de passe ('default'). Il est important que ce mot de passe soit modifié en quelque-chose de sécurisé et unique.]]> + ('admin') et le mot de passe ('default'). Il est important que ce mot de passe soit modifié en quelque-chose de sécurisé et unique. + ]]> Pour bien commencer, regardez nos vidéos d'introduction En cliquant sur le bouton "Suivant" (ou en modifiant umbracoConfigurationStatus dans le fichier web.config), vous acceptez la licence de ce logiciel telle que spécifiée dans le champ ci-dessous. Veuillez noter que cette distribution Umbraco consiste en deux licences différentes, la licence open source MIT pour le framework et la licence Umbraco freeware qui couvre l'UI. Pas encore installé. @@ -690,7 +700,8 @@ Pour gérer votre site, ouvrez simplement le backoffice Umbraco et commencez à Joyeux vendredi Joyeux samedi Connectez-vous ci-dessous - La session a expiré + Identifiez-vous avec + La session a expiré © 2001 - %0%
Umbraco.com

]]>
Mot de passe oublié? Un email contenant un lien pour ré-initialiser votre mot de passe sera envoyé à l'adresse spécifiée @@ -800,6 +811,14 @@ Pour gérer votre site, ouvrez simplement le backoffice Umbraco et commencez à Version du package Historique des versions du package Voir le site internet du package + Package déjà installé + Ce package ne peut pas être installé, il nécessite au minimum la version Umbraco %0% + Désinstallation... + Téléchargement... + Import... + Installation... + Redémarrage, veuillez patienter... + Terminé, votre navigateur va être rafraîchi, veuillez patienter... Coller en conservant le formatage (non recommandé) @@ -1401,4 +1420,22 @@ Pour gérer votre site, ouvrez simplement le backoffice Umbraco et commencez à %0%.]]> %0%.]]> - + + + Désactiver URL tracker + Activer URL tracker + URL original + Redirigé Vers + Aucune redirection n'a été créée + Lorsqu'une page publiée est renommée ou déplacée, une redirection sera automatiquement créée vers la nouvelle page. + Supprimer + Etes-vous certain(e) de vouloir supprimer la redirection de '%0%' vers '%1%'? + Redirection d'URL supprimée. + Erreur lors de la suppression de la redirection d'URL. + Etes-vous certain(e) de vouloir désactiver le URL tracker? + URL tracker est maintenant désactivé. + Erreur lors de la désactivation de l'URL tracker, plus d'information disponible dans votre fichier log. + URL tracker est maintenant activé. + Erreur lors de l'activation de l'URL tracker, plus d'information disponible dans votre fichier log. + + From f0db4824ec26595a03fd26fbb95380f0a4492394 Mon Sep 17 00:00:00 2001 From: michael Date: Fri, 30 Sep 2016 23:20:17 +0200 Subject: [PATCH 12/48] Solve issue U4-8782 : E-mail validation when editing user + update error message when creating new user to use localized text --- src/Umbraco.Web.UI/umbraco/create/User.ascx | 2 +- .../umbraco/users/EditUser.aspx.cs | 24 +++++++++++++++++++ 2 files changed, 25 insertions(+), 1 deletion(-) diff --git a/src/Umbraco.Web.UI/umbraco/create/User.ascx b/src/Umbraco.Web.UI/umbraco/create/User.ascx index deb139c557..3a00811776 100644 --- a/src/Umbraco.Web.UI/umbraco/create/User.ascx +++ b/src/Umbraco.Web.UI/umbraco/create/User.ascx @@ -22,7 +22,7 @@ ControlToValidate="Email" ValidateEmptyText="false" OnServerValidate="EmailExistsCheck">
diff --git a/src/Umbraco.Web/umbraco.presentation/umbraco/users/EditUser.aspx.cs b/src/Umbraco.Web/umbraco.presentation/umbraco/users/EditUser.aspx.cs index d9605506b4..bec91d3313 100644 --- a/src/Umbraco.Web/umbraco.presentation/umbraco/users/EditUser.aspx.cs +++ b/src/Umbraco.Web/umbraco.presentation/umbraco/users/EditUser.aspx.cs @@ -173,9 +173,29 @@ namespace umbraco.cms.presentation.user passw.Controls.Add(passwordChanger); passw.Controls.Add(validatorContainer); + // Add email validator + var emailValidation = new CustomValidator + { + ID = "EmailValidator", + ErrorMessage = ui.Text("errorHandling", "errorRegExpWithoutTab", "E-mail", CurrentUser), + Display = ValidatorDisplay.None + }; + emailValidation.ServerValidate += EmailValidator_OnServerValidate; + + var validationSummary = new ValidationSummary + { + ID = "validationSummary", + DisplayMode = ValidationSummaryDisplayMode.BulletList, + CssClass = "error" + }; + + pp.addProperty(validationSummary); + pp.addProperty(ui.Text("user", "username", UmbracoUser), uname); pp.addProperty(ui.Text("user", "loginname", UmbracoUser), lname); pp.addProperty(ui.Text("user", "password", UmbracoUser), passw); + + pp.addProperty(emailValidation); pp.addProperty(ui.Text("email", UmbracoUser), email); pp.addProperty(ui.Text("user", "usertype", UmbracoUser), userType); pp.addProperty(ui.Text("user", "language", UmbracoUser), userLanguage); @@ -227,6 +247,10 @@ namespace umbraco.cms.presentation.user .SyncTree(UID.ToString(), IsPostBack); } + private void EmailValidator_OnServerValidate(object source, ServerValidateEventArgs args) + { + args.IsValid = MembershipProviderBase.IsEmailValid(email.Text); + } void sectionValidator_ServerValidate(object source, ServerValidateEventArgs args) { From f1c8fb7b17e3676203bcf2a5c91fcd1887d5b40a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dennis=20J=C3=B8rgensen?= Date: Sat, 15 Oct 2016 22:00:03 +0200 Subject: [PATCH 13/48] Added validation to the user edit-form in the backoffice --- .../umbraco/users/EditUser.aspx.cs | 77 +++++++++++++++++-- src/umbraco.controls/pane.cs | 16 ++++ 2 files changed, 86 insertions(+), 7 deletions(-) diff --git a/src/Umbraco.Web/umbraco.presentation/umbraco/users/EditUser.aspx.cs b/src/Umbraco.Web/umbraco.presentation/umbraco/users/EditUser.aspx.cs index d9605506b4..a25d62665b 100644 --- a/src/Umbraco.Web/umbraco.presentation/umbraco/users/EditUser.aspx.cs +++ b/src/Umbraco.Web/umbraco.presentation/umbraco/users/EditUser.aspx.cs @@ -30,6 +30,8 @@ using Umbraco.Core; using Umbraco.Core.Models; using Umbraco.Core.Services; using PropertyType = umbraco.cms.businesslogic.propertytype.PropertyType; +using System.Text.RegularExpressions; +using System.Text; namespace umbraco.cms.presentation.user { @@ -43,11 +45,16 @@ namespace umbraco.cms.presentation.user CurrentApp = DefaultApps.users.ToString(); } protected HtmlTable macroProperties; - protected TextBox uname = new TextBox(); - protected TextBox lname = new TextBox(); + protected TextBox uname = new TextBox() { ID = "uname" }; + protected RequiredFieldValidator unameValidator = new RequiredFieldValidator(); + protected TextBox lname = new TextBox() { ID = "lname" }; + protected RequiredFieldValidator lnameValidator = new RequiredFieldValidator(); + protected CustomValidator lnameCustomValidator = new CustomValidator(); protected PlaceHolder passw = new PlaceHolder(); protected CheckBoxList lapps = new CheckBoxList(); - protected TextBox email = new TextBox(); + protected TextBox email = new TextBox() { ID = "email" }; + protected RequiredFieldValidator emailValidator = new RequiredFieldValidator(); + protected CustomValidator emailCustomValidator = new CustomValidator(); protected DropDownList userType = new DropDownList(); protected DropDownList userLanguage = new DropDownList(); protected CheckBox NoConsole = new CheckBox(); @@ -173,10 +180,10 @@ namespace umbraco.cms.presentation.user passw.Controls.Add(passwordChanger); passw.Controls.Add(validatorContainer); - pp.addProperty(ui.Text("user", "username", UmbracoUser), uname); - pp.addProperty(ui.Text("user", "loginname", UmbracoUser), lname); + pp.addProperty(ui.Text("user", "username", UmbracoUser), uname, unameValidator); + pp.addProperty(ui.Text("user", "loginname", UmbracoUser), lname, lnameValidator, lnameCustomValidator); pp.addProperty(ui.Text("user", "password", UmbracoUser), passw); - pp.addProperty(ui.Text("email", UmbracoUser), email); + pp.addProperty(ui.Text("email", UmbracoUser), email, emailValidator, emailCustomValidator); pp.addProperty(ui.Text("user", "usertype", UmbracoUser), userType); pp.addProperty(ui.Text("user", "language", UmbracoUser), userLanguage); @@ -219,6 +226,50 @@ namespace umbraco.cms.presentation.user sectionValidator.CssClass = "error"; sectionValidator.Style.Add("color", "red"); + unameValidator.ControlToValidate = uname.ID; + unameValidator.Display = ValidatorDisplay.Dynamic; + unameValidator.ErrorMessage = ui.Text("defaultdialogs", "requiredField", UmbracoUser); + unameValidator.CssClass = "error"; + unameValidator.Style.Add("color", "red"); + unameValidator.Style.Add("margin-left", "5px"); + unameValidator.Style.Add("line-height", "28px"); + + lnameValidator.ControlToValidate = lname.ID; + lnameValidator.Display = ValidatorDisplay.Dynamic; + lnameValidator.ErrorMessage = ui.Text("defaultdialogs", "requiredField", UmbracoUser); + lnameValidator.CssClass = "error"; + lnameValidator.Style.Add("color", "red"); + lnameValidator.Style.Add("margin-left", "5px"); + lnameValidator.Style.Add("line-height", "28px"); + + lnameCustomValidator.ServerValidate += LnameCustomValidator_ServerValidate; + lnameCustomValidator.Display = ValidatorDisplay.Dynamic; + lnameCustomValidator.ControlToValidate = lname.ID; + var localizedLname = ui.Text("user", "loginname", UmbracoUser); + lnameCustomValidator.ErrorMessage = ui.Text("errorHandling", "errorExistsWithoutTab", localizedLname, UmbracoUser); + lnameCustomValidator.CssClass = "error"; + lnameCustomValidator.Style.Add("color", "red"); + lnameCustomValidator.Style.Add("margin-left", "5px"); + lnameCustomValidator.Style.Add("line-height", "28px"); + + emailValidator.ControlToValidate = email.ID; + emailValidator.Display = ValidatorDisplay.Dynamic; + emailValidator.ErrorMessage = ui.Text("defaultdialogs", "requiredField", UmbracoUser); + emailValidator.CssClass = "error"; + emailValidator.Style.Add("color", "red"); + emailValidator.Style.Add("margin-left", "5px"); + emailValidator.Style.Add("line-height", "28px"); + + emailCustomValidator.ServerValidate += EmailCustomValidator_ServerValidate; + emailCustomValidator.Display = ValidatorDisplay.Dynamic; + emailCustomValidator.ControlToValidate = email.ID; + var localizedEmail = ui.Text("general", "email", UmbracoUser); + emailCustomValidator.ErrorMessage = ui.Text("errorHandling", "errorRegExpWithoutTab", localizedEmail, UmbracoUser); + emailCustomValidator.CssClass = "error"; + emailCustomValidator.Style.Add("color", "red"); + emailCustomValidator.Style.Add("margin-left", "5px"); + emailCustomValidator.Style.Add("line-height", "28px"); + SetupForm(); SetupChannel(); @@ -227,6 +278,16 @@ namespace umbraco.cms.presentation.user .SyncTree(UID.ToString(), IsPostBack); } + private void LnameCustomValidator_ServerValidate(object source, ServerValidateEventArgs args) + { + var usersWithLoginName = ApplicationContext.Services.UserService.GetByUsername(lname.Text); + args.IsValid = usersWithLoginName == null || usersWithLoginName.Id == u.Id; + } + + private void EmailCustomValidator_ServerValidate(object source, ServerValidateEventArgs args) + { + args.IsValid = Regex.IsMatch(email.Text.Trim(), @"^[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Za-z]{2,}$"); + } void sectionValidator_ServerValidate(object source, ServerValidateEventArgs args) { @@ -531,7 +592,9 @@ namespace umbraco.cms.presentation.user } else { - ClientTools.ShowSpeechBubble(speechBubbleIcon.error, ui.Text("speechBubbles", "editUserError", UmbracoUser), ""); + ClientTools.ShowSpeechBubble(speechBubbleIcon.error, + ui.Text("speechBubbles", "validationFailedHeader", UmbracoUser), + ui.Text("speechBubbles", "validationFailedMessage", UmbracoUser)); } } diff --git a/src/umbraco.controls/pane.cs b/src/umbraco.controls/pane.cs index 0facc946a3..499695debf 100644 --- a/src/umbraco.controls/pane.cs +++ b/src/umbraco.controls/pane.cs @@ -36,6 +36,22 @@ namespace umbraco.uicontrols set { m_title = value; } } + public void addProperty(string Caption, Control C, params BaseValidator[] validators) + { + + PropertyPanel pp = new PropertyPanel(); + pp.Controls.Add(C); + + foreach (var validator in validators) + { + validator.Display = ValidatorDisplay.Dynamic; + pp.Controls.Add(validator); + } + pp.Text = Caption; + + this.Controls.Add(pp); + } + public void addProperty(string Caption, Control C) { From ac0159f67e6fe14193db98626f891785aac7c667 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dennis=20J=C3=B8rgensen?= Date: Sat, 15 Oct 2016 22:47:35 +0200 Subject: [PATCH 14/48] Changed style for form controls and text for email-field --- src/Umbraco.Web.UI.Client/src/less/main.less | 2 +- .../umbraco.presentation/umbraco/users/EditUser.aspx.cs | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Umbraco.Web.UI.Client/src/less/main.less b/src/Umbraco.Web.UI.Client/src/less/main.less index 00cdd4f9b1..85ab4e6e56 100644 --- a/src/Umbraco.Web.UI.Client/src/less/main.less +++ b/src/Umbraco.Web.UI.Client/src/less/main.less @@ -157,7 +157,7 @@ h5.-black { } .controls-row { - padding-top: 5px; + padding-bottom: 5px; margin-left: 240px; } diff --git a/src/Umbraco.Web/umbraco.presentation/umbraco/users/EditUser.aspx.cs b/src/Umbraco.Web/umbraco.presentation/umbraco/users/EditUser.aspx.cs index a25d62665b..d371016910 100644 --- a/src/Umbraco.Web/umbraco.presentation/umbraco/users/EditUser.aspx.cs +++ b/src/Umbraco.Web/umbraco.presentation/umbraco/users/EditUser.aspx.cs @@ -183,7 +183,7 @@ namespace umbraco.cms.presentation.user pp.addProperty(ui.Text("user", "username", UmbracoUser), uname, unameValidator); pp.addProperty(ui.Text("user", "loginname", UmbracoUser), lname, lnameValidator, lnameCustomValidator); pp.addProperty(ui.Text("user", "password", UmbracoUser), passw); - pp.addProperty(ui.Text("email", UmbracoUser), email, emailValidator, emailCustomValidator); + pp.addProperty(ui.Text("general", "email", UmbracoUser), email, emailValidator, emailCustomValidator); pp.addProperty(ui.Text("user", "usertype", UmbracoUser), userType); pp.addProperty(ui.Text("user", "language", UmbracoUser), userLanguage); From fb83b9dca3e6dc208770607e028f64725003c09f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dennis=20J=C3=B8rgensen?= Date: Sun, 16 Oct 2016 18:45:01 +0200 Subject: [PATCH 15/48] Changed email validation to the validation used when creating users (MembershipProviderBase). --- .../umbraco.presentation/umbraco/users/EditUser.aspx.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Umbraco.Web/umbraco.presentation/umbraco/users/EditUser.aspx.cs b/src/Umbraco.Web/umbraco.presentation/umbraco/users/EditUser.aspx.cs index d371016910..0006218cc7 100644 --- a/src/Umbraco.Web/umbraco.presentation/umbraco/users/EditUser.aspx.cs +++ b/src/Umbraco.Web/umbraco.presentation/umbraco/users/EditUser.aspx.cs @@ -286,7 +286,7 @@ namespace umbraco.cms.presentation.user private void EmailCustomValidator_ServerValidate(object source, ServerValidateEventArgs args) { - args.IsValid = Regex.IsMatch(email.Text.Trim(), @"^[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Za-z]{2,}$"); + args.IsValid = MembershipProviderBase.IsEmailValid(email.Text.Trim()); } void sectionValidator_ServerValidate(object source, ServerValidateEventArgs args) From f753dfa2a710917ca863c5c0b33ad21425f9a2f6 Mon Sep 17 00:00:00 2001 From: Claus Date: Thu, 20 Oct 2016 13:56:52 +0200 Subject: [PATCH 16/48] minor updates and a little reformatting. --- src/Umbraco.Web.UI/umbraco/config/lang/da.xml | 4 +- src/Umbraco.Web.UI/umbraco/config/lang/en.xml | 2 +- .../umbraco/config/lang/en_us.xml | 2 +- .../settings/EditDictionaryItem.aspx.cs | 216 +++++++++--------- 4 files changed, 107 insertions(+), 117 deletions(-) diff --git a/src/Umbraco.Web.UI/umbraco/config/lang/da.xml b/src/Umbraco.Web.UI/umbraco/config/lang/da.xml index 130878323e..ffb0a9d96b 100644 --- a/src/Umbraco.Web.UI/umbraco/config/lang/da.xml +++ b/src/Umbraco.Web.UI/umbraco/config/lang/da.xml @@ -308,10 +308,10 @@ Rediger de forskellige sprogversioner for ordbogselementet '%0%' herunder.
Du tilføjer flere sprog under 'sprog' i menuen til venstre
]]>
Kulturnavn - Her kan du ændre nøglen på ordbogselementet. + Rediger navnet på ordbogselementet. diff --git a/src/Umbraco.Web.UI/umbraco/config/lang/en.xml b/src/Umbraco.Web.UI/umbraco/config/lang/en.xml index 38376231b7..f710e54215 100644 --- a/src/Umbraco.Web.UI/umbraco/config/lang/en.xml +++ b/src/Umbraco.Web.UI/umbraco/config/lang/en.xml @@ -327,7 +327,7 @@ Edit the different language versions for the dictionary item '%0%' below
You can add additional languages under the 'languages' in the menu on the left ]]>
Culture Name - Here you can change the key of the dictionary item. + Edit the key of the dictionary item. %0%' below
You can add additional languages under the 'languages' in the menu on the left ]]>
Culture Name - Here you can change the key of the dictionary item. + Edit the key of the dictionary item. - /// Summary description for EditDictionaryItem. - /// + /// + /// Summary description for EditDictionaryItem. + /// [WebformsPageTreeAuthorize(Constants.Trees.Dictionary)] - public partial class EditDictionaryItem : BasePages.UmbracoEnsuredPage - { - - protected LiteralControl keyTxt = new LiteralControl(); - protected uicontrols.TabView tbv = new uicontrols.TabView(); - private System.Collections.ArrayList languageFields = new System.Collections.ArrayList(); + public partial class EditDictionaryItem : BasePages.UmbracoEnsuredPage + { + + protected LiteralControl keyTxt = new LiteralControl(); + protected uicontrols.TabView tbv = new uicontrols.TabView(); + private System.Collections.ArrayList languageFields = new System.Collections.ArrayList(); private cms.businesslogic.Dictionary.DictionaryItem currentItem; - protected TextBox boxChangeKey; - protected Label labelChangeKey; - protected User currentUser; + protected TextBox boxChangeKey; + protected Label labelChangeKey; + protected Literal txt; + protected User currentUser; - protected void Page_Load(object sender, System.EventArgs e) - { - currentItem = new cms.businesslogic.Dictionary.DictionaryItem(int.Parse(Request.QueryString["id"])); - currentUser = getUser(); + protected void Page_Load(object sender, System.EventArgs e) + { + currentItem = new cms.businesslogic.Dictionary.DictionaryItem(int.Parse(Request.QueryString["id"])); + currentUser = getUser(); - // Put user code to initialize the page here - Panel1.hasMenu = true; - Panel1.Text = ui.Text("editdictionary") + ": " + currentItem.key; + // Put user code to initialize the page here + Panel1.hasMenu = true; + Panel1.Text = ui.Text("editdictionary") + ": " + currentItem.key; - var save = Panel1.Menu.NewButton(); + var save = Panel1.Menu.NewButton(); save.Text = ui.Text("save"); save.Click += save_Click; - save.ToolTip = ui.Text("save"); + save.ToolTip = ui.Text("save"); save.ID = "save"; save.ButtonType = uicontrols.MenuButtonType.Primary; uicontrols.Pane p = new uicontrols.Pane(); - Literal txt = new Literal(); - txt.Text = "

" + ui.Text("dictionaryItem", "description", currentItem.key, currentUser) + "


"; - p.addProperty(txt); - - foreach (cms.businesslogic.language.Language l in cms.businesslogic.language.Language.getAll) - { - - TextBox languageBox = new TextBox(); - languageBox.TextMode = TextBoxMode.MultiLine; - languageBox.ID = l.id.ToString(); - languageBox.CssClass = "umbEditorTextFieldMultiple"; - - if (!IsPostBack) - languageBox.Text = currentItem.Value(l.id); - - languageFields.Add(languageBox); - p.addProperty(l.FriendlyName, languageBox); - - } - boxChangeKey = new TextBox { ID = "changeKey-" + currentItem.id, @@ -81,42 +54,64 @@ namespace umbraco.settings ID = "changeKeyLabel", CssClass = "text-error" }; - + p.addProperty(new Literal { - Text = "

 

" + ui.Text("dictionaryItem", "changeKey", currentUser) + "

" + Text = "

" + ui.Text("dictionaryItem", "changeKey", currentUser) + "

" }); p.addProperty(boxChangeKey); p.addProperty(labelChangeKey); + + txt = new Literal(); + txt.Text = "

" + ui.Text("dictionaryItem", "description", currentItem.key, currentUser) + "


"; + p.addProperty(txt); + + foreach (cms.businesslogic.language.Language l in cms.businesslogic.language.Language.getAll) + { + + TextBox languageBox = new TextBox(); + languageBox.TextMode = TextBoxMode.MultiLine; + languageBox.ID = l.id.ToString(); + languageBox.CssClass = "umbEditorTextFieldMultiple"; + + if (!IsPostBack) + languageBox.Text = currentItem.Value(l.id); + + languageFields.Add(languageBox); + p.addProperty(l.FriendlyName, languageBox); + + } + + if (!IsPostBack) - { - var path = BuildPath(currentItem); - ClientTools - .SetActiveTreeType(TreeDefinitionCollection.Instance.FindTree().Tree.Alias) - .SyncTree(path, false); - } + { + var path = BuildPath(currentItem); + ClientTools + .SetActiveTreeType(TreeDefinitionCollection.Instance.FindTree().Tree.Alias) + .SyncTree(path, false); + } Panel1.Controls.Add(p); - } + } - private string BuildPath(cms.businesslogic.Dictionary.DictionaryItem current) - { - var parentPath = current.IsTopMostItem() ? "" : BuildPath(current.Parent) + ","; - return parentPath + current.id; - } + private string BuildPath(cms.businesslogic.Dictionary.DictionaryItem current) + { + var parentPath = current.IsTopMostItem() ? "" : BuildPath(current.Parent) + ","; + return parentPath + current.id; + } void save_Click(object sender, EventArgs e) { - foreach (TextBox t in languageFields) + foreach (TextBox t in languageFields) { //check for null but allow empty string! // http://issues.umbraco.org/issue/U4-1931 - if (t.Text != null) + if (t.Text != null) { - currentItem.setValue(int.Parse(t.ID),t.Text); - } - } + currentItem.setValue(int.Parse(t.ID), t.Text); + } + } labelChangeKey.Text = ""; // reset error text var newKey = boxChangeKey.Text; @@ -140,55 +135,50 @@ namespace umbraco.settings var path = BuildPath(currentItem); ClientTools.SyncTree(path, true); } - } + } + txt.Text = "

" + ui.Text("dictionaryItem", "description", currentItem.key, currentUser) + "


"; + ClientTools.ShowSpeechBubble(speechBubbleIcon.save, ui.Text("speechBubbles", "dictionaryItemSaved"), ""); + } - ClientTools.ShowSpeechBubble(speechBubbleIcon.save, ui.Text("speechBubbles", "dictionaryItemSaved"), ""); - } + #region Web Form Designer generated code - #region Web Form Designer generated code - override protected void OnInit(EventArgs e) - { - // - // CODEGEN: This call is required by the ASP.NET Web Form Designer. - // - /* - tbv.ID="tabview1"; - tbv.Width = 400; - tbv.Height = 200; - */ + override protected void OnInit(EventArgs e) + { + InitializeComponent(); + base.OnInit(e); + } - InitializeComponent(); - base.OnInit(e); - } - - /// - /// Required method for Designer support - do not modify - /// the contents of this method with the code editor. - /// - private void InitializeComponent() - { - } - #endregion + /// + /// Required method for Designer support - do not modify + /// the contents of this method with the code editor. + /// + private void InitializeComponent() + { + } + + #endregion + private class languageTextbox : TextBox + { - private class languageTextbox : TextBox - { - - private int _languageid; - public int languageid - { - set {_languageid = value;} - get {return _languageid;} - } - public languageTextbox(int languageId) : base() { - this.TextMode = TextBoxMode.MultiLine; - this.Rows = 10; - this.Columns = 40; - this.Attributes.Add("style", "margin: 3px; width: 98%;"); - - this.languageid = languageId; - } - } - } + private int _languageid; + + public int languageid + { + set { _languageid = value; } + get { return _languageid; } + } + + public languageTextbox(int languageId) : base() + { + this.TextMode = TextBoxMode.MultiLine; + this.Rows = 10; + this.Columns = 40; + this.Attributes.Add("style", "margin: 3px; width: 98%;"); + + this.languageid = languageId; + } + } + } } From ee8efbb1ab66498425da856d2dca69ba4063e16e Mon Sep 17 00:00:00 2001 From: Shannon Date: Fri, 21 Oct 2016 16:11:57 +0200 Subject: [PATCH 17/48] Adds notes about bulk copy --- src/Umbraco.Core/Persistence/PetaPocoExtensions.cs | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/src/Umbraco.Core/Persistence/PetaPocoExtensions.cs b/src/Umbraco.Core/Persistence/PetaPocoExtensions.cs index a72621d1a5..610b3d9b02 100644 --- a/src/Umbraco.Core/Persistence/PetaPocoExtensions.cs +++ b/src/Umbraco.Core/Persistence/PetaPocoExtensions.cs @@ -195,6 +195,16 @@ namespace Umbraco.Core.Persistence /// public static void BulkInsertRecords(this Database db, IEnumerable collection, Transaction tr, bool commitTrans = false) { + //TODO: We should change this to use BulkCopy, as an example see: + // https://ayende.com/blog/4137/nhibernate-perf-tricks + // Even though this just generates lots of raw sql INSERT statements BulkCopy is the fastest it can possibly be + // and we should be able to do this using the current connection from the PetaPoco db instance (and would probably be much cleaner) + // + // BulkCopy is available for SQL Server and MySqlBulkLoader is available for MySql, pretty sure BulkCopy works for SQLCE so + // we should be covered and of course could fallback to this method if that is not our database. But we would get huge perf + // increases for this. + + //don't do anything if there are no records. if (collection.Any() == false) return; From d64757f3b07c12e67c02a9127eb419da9d67c9ab Mon Sep 17 00:00:00 2001 From: Shannon Date: Fri, 21 Oct 2016 16:17:13 +0200 Subject: [PATCH 18/48] U4-9093 Fix SQL used to rebuild the media & content xml structures in the data integrity check --- .../Repositories/ContentRepository.cs | 29 ++++++++----------- 1 file changed, 12 insertions(+), 17 deletions(-) diff --git a/src/Umbraco.Core/Persistence/Repositories/ContentRepository.cs b/src/Umbraco.Core/Persistence/Repositories/ContentRepository.cs index 9222769247..b4ff7be361 100644 --- a/src/Umbraco.Core/Persistence/Repositories/ContentRepository.cs +++ b/src/Umbraco.Core/Persistence/Repositories/ContentRepository.cs @@ -183,30 +183,25 @@ namespace Umbraco.Core.Persistence.Repositories if (contentTypeIds == null) { var subQuery = new Sql() - .Select("DISTINCT cmsContentXml.nodeId") - .From() - .InnerJoin() - .On(left => left.NodeId, right => right.NodeId); + .Select("id") + .From(SqlSyntax) + .Where(x => x.NodeObjectType == NodeObjectTypeId); var deleteSql = SqlSyntax.GetDeleteSubquery("cmsContentXml", "nodeId", subQuery); Database.Execute(deleteSql); } else { - foreach (var id in contentTypeIds) - { - var id1 = id; - var subQuery = new Sql() - .Select("cmsDocument.nodeId") - .From() - .InnerJoin() - .On(left => left.NodeId, right => right.NodeId) - .Where(dto => dto.Published) - .Where(dto => dto.ContentTypeId == id1); + var subQuery = new Sql() + .Select("DISTINCT umbracoNode.id") + .From(SqlSyntax) + .InnerJoin(SqlSyntax) + .On(SqlSyntax, left => left.NodeId, right => right.NodeId) + .WhereIn(dto => dto.ContentTypeId, contentTypeIds, SqlSyntax) + .Where(x => x.NodeObjectType == NodeObjectTypeId); - var deleteSql = SqlSyntax.GetDeleteSubquery("cmsContentXml", "nodeId", subQuery); - Database.Execute(deleteSql); - } + var deleteSql = SqlSyntax.GetDeleteSubquery("cmsContentXml", "nodeId", subQuery); + Database.Execute(deleteSql); } //now insert the data, again if something fails here, the whole transaction is reversed From 4f77caee0297072d885c7194aa2a84af118db2c0 Mon Sep 17 00:00:00 2001 From: Shannon Date: Fri, 21 Oct 2016 16:23:32 +0200 Subject: [PATCH 19/48] U4-9093 Fix SQL used to rebuild the media & content xml structures in the data integrity check --- .../Repositories/MediaRepository.cs | 34 +++++++------------ .../Repositories/MemberRepository.cs | 34 +++++++------------ 2 files changed, 24 insertions(+), 44 deletions(-) diff --git a/src/Umbraco.Core/Persistence/Repositories/MediaRepository.cs b/src/Umbraco.Core/Persistence/Repositories/MediaRepository.cs index 8c9bec71d4..e311ac9297 100644 --- a/src/Umbraco.Core/Persistence/Repositories/MediaRepository.cs +++ b/src/Umbraco.Core/Persistence/Repositories/MediaRepository.cs @@ -172,36 +172,26 @@ namespace Umbraco.Core.Persistence.Repositories //Remove all the data first, if anything fails after this it's no problem the transaction will be reverted if (contentTypeIds == null) { - var mediaObjectType = Guid.Parse(Constants.ObjectTypes.Media); var subQuery = new Sql() - .Select("DISTINCT cmsContentXml.nodeId") - .From() - .InnerJoin() - .On(left => left.NodeId, right => right.NodeId) - .Where(dto => dto.NodeObjectType == mediaObjectType); + .Select("id") + .From(SqlSyntax) + .Where(x => x.NodeObjectType == NodeObjectTypeId); var deleteSql = SqlSyntax.GetDeleteSubquery("cmsContentXml", "nodeId", subQuery); Database.Execute(deleteSql); } else { - foreach (var id in contentTypeIds) - { - var id1 = id; - var mediaObjectType = Guid.Parse(Constants.ObjectTypes.Media); - var subQuery = new Sql() - .Select("DISTINCT cmsContentXml.nodeId") - .From() - .InnerJoin() - .On(left => left.NodeId, right => right.NodeId) - .InnerJoin() - .On(left => left.NodeId, right => right.NodeId) - .Where(dto => dto.NodeObjectType == mediaObjectType) - .Where(dto => dto.ContentTypeId == id1); + var subQuery = new Sql() + .Select("DISTINCT umbracoNode.id") + .From(SqlSyntax) + .InnerJoin(SqlSyntax) + .On(SqlSyntax, left => left.NodeId, right => right.NodeId) + .WhereIn(dto => dto.ContentTypeId, contentTypeIds, SqlSyntax) + .Where(x => x.NodeObjectType == NodeObjectTypeId); - var deleteSql = SqlSyntax.GetDeleteSubquery("cmsContentXml", "nodeId", subQuery); - Database.Execute(deleteSql); - } + var deleteSql = SqlSyntax.GetDeleteSubquery("cmsContentXml", "nodeId", subQuery); + Database.Execute(deleteSql); } //now insert the data, again if something fails here, the whole transaction is reversed diff --git a/src/Umbraco.Core/Persistence/Repositories/MemberRepository.cs b/src/Umbraco.Core/Persistence/Repositories/MemberRepository.cs index 0f0e797f17..379139b4f7 100644 --- a/src/Umbraco.Core/Persistence/Repositories/MemberRepository.cs +++ b/src/Umbraco.Core/Persistence/Repositories/MemberRepository.cs @@ -389,36 +389,26 @@ namespace Umbraco.Core.Persistence.Repositories //Remove all the data first, if anything fails after this it's no problem the transaction will be reverted if (contentTypeIds == null) { - var memberObjectType = Guid.Parse(Constants.ObjectTypes.Member); var subQuery = new Sql() - .Select("DISTINCT cmsContentXml.nodeId") - .From() - .InnerJoin() - .On(left => left.NodeId, right => right.NodeId) - .Where(dto => dto.NodeObjectType == memberObjectType); + .Select("id") + .From(SqlSyntax) + .Where(x => x.NodeObjectType == NodeObjectTypeId); var deleteSql = SqlSyntax.GetDeleteSubquery("cmsContentXml", "nodeId", subQuery); Database.Execute(deleteSql); } else { - foreach (var id in contentTypeIds) - { - var id1 = id; - var memberObjectType = Guid.Parse(Constants.ObjectTypes.Member); - var subQuery = new Sql() - .Select("DISTINCT cmsContentXml.nodeId") - .From() - .InnerJoin() - .On(left => left.NodeId, right => right.NodeId) - .InnerJoin() - .On(left => left.NodeId, right => right.NodeId) - .Where(dto => dto.NodeObjectType == memberObjectType) - .Where(dto => dto.ContentTypeId == id1); + var subQuery = new Sql() + .Select("DISTINCT umbracoNode.id") + .From(SqlSyntax) + .InnerJoin(SqlSyntax) + .On(SqlSyntax, left => left.NodeId, right => right.NodeId) + .WhereIn(dto => dto.ContentTypeId, contentTypeIds, SqlSyntax) + .Where(x => x.NodeObjectType == NodeObjectTypeId); - var deleteSql = SqlSyntax.GetDeleteSubquery("cmsContentXml", "nodeId", subQuery); - Database.Execute(deleteSql); - } + var deleteSql = SqlSyntax.GetDeleteSubquery("cmsContentXml", "nodeId", subQuery); + Database.Execute(deleteSql); } //now insert the data, again if something fails here, the whole transaction is reversed From 1a8f53d1dec180220bd56ec617191c9f6b3225a8 Mon Sep 17 00:00:00 2001 From: Shannon Date: Fri, 21 Oct 2016 18:16:02 +0200 Subject: [PATCH 20/48] Fixes tests --- .../Persistence/Repositories/ContentRepository.cs | 6 ++++-- .../Persistence/Repositories/MediaRepository.cs | 2 +- .../Persistence/Repositories/MemberRepository.cs | 2 +- 3 files changed, 6 insertions(+), 4 deletions(-) diff --git a/src/Umbraco.Core/Persistence/Repositories/ContentRepository.cs b/src/Umbraco.Core/Persistence/Repositories/ContentRepository.cs index b4ff7be361..7d61470747 100644 --- a/src/Umbraco.Core/Persistence/Repositories/ContentRepository.cs +++ b/src/Umbraco.Core/Persistence/Repositories/ContentRepository.cs @@ -193,8 +193,10 @@ namespace Umbraco.Core.Persistence.Repositories else { var subQuery = new Sql() - .Select("DISTINCT umbracoNode.id") - .From(SqlSyntax) + .Select("umbracoNode.id as nodeId") + .From(SqlSyntax) + .InnerJoin(SqlSyntax) + .On(SqlSyntax, left => left.NodeId, right => right.NodeId) .InnerJoin(SqlSyntax) .On(SqlSyntax, left => left.NodeId, right => right.NodeId) .WhereIn(dto => dto.ContentTypeId, contentTypeIds, SqlSyntax) diff --git a/src/Umbraco.Core/Persistence/Repositories/MediaRepository.cs b/src/Umbraco.Core/Persistence/Repositories/MediaRepository.cs index e311ac9297..598c9e912d 100644 --- a/src/Umbraco.Core/Persistence/Repositories/MediaRepository.cs +++ b/src/Umbraco.Core/Persistence/Repositories/MediaRepository.cs @@ -183,7 +183,7 @@ namespace Umbraco.Core.Persistence.Repositories else { var subQuery = new Sql() - .Select("DISTINCT umbracoNode.id") + .Select("umbracoNode.id as nodeId") .From(SqlSyntax) .InnerJoin(SqlSyntax) .On(SqlSyntax, left => left.NodeId, right => right.NodeId) diff --git a/src/Umbraco.Core/Persistence/Repositories/MemberRepository.cs b/src/Umbraco.Core/Persistence/Repositories/MemberRepository.cs index 379139b4f7..216fc223db 100644 --- a/src/Umbraco.Core/Persistence/Repositories/MemberRepository.cs +++ b/src/Umbraco.Core/Persistence/Repositories/MemberRepository.cs @@ -400,7 +400,7 @@ namespace Umbraco.Core.Persistence.Repositories else { var subQuery = new Sql() - .Select("DISTINCT umbracoNode.id") + .Select("umbracoNode.id as nodeId") .From(SqlSyntax) .InnerJoin(SqlSyntax) .On(SqlSyntax, left => left.NodeId, right => right.NodeId) From 92f5ad220ac9b12556e38a7e883b3ec58b80223e Mon Sep 17 00:00:00 2001 From: Shannon Date: Fri, 21 Oct 2016 18:17:31 +0200 Subject: [PATCH 21/48] oops, changes back so we have less joins, we don't need to query on cmsDocument or published, we know it will be that --- .../Persistence/Repositories/ContentRepository.cs | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/src/Umbraco.Core/Persistence/Repositories/ContentRepository.cs b/src/Umbraco.Core/Persistence/Repositories/ContentRepository.cs index 7d61470747..cafbabd286 100644 --- a/src/Umbraco.Core/Persistence/Repositories/ContentRepository.cs +++ b/src/Umbraco.Core/Persistence/Repositories/ContentRepository.cs @@ -194,9 +194,7 @@ namespace Umbraco.Core.Persistence.Repositories { var subQuery = new Sql() .Select("umbracoNode.id as nodeId") - .From(SqlSyntax) - .InnerJoin(SqlSyntax) - .On(SqlSyntax, left => left.NodeId, right => right.NodeId) + .From(SqlSyntax) .InnerJoin(SqlSyntax) .On(SqlSyntax, left => left.NodeId, right => right.NodeId) .WhereIn(dto => dto.ContentTypeId, contentTypeIds, SqlSyntax) From dadcbc8b31248d24d1f9fcf95af4e884f0d51cf1 Mon Sep 17 00:00:00 2001 From: Shannon Date: Tue, 25 Oct 2016 12:29:17 +0200 Subject: [PATCH 22/48] U4-9107 Change BulkInsertRecords to use BulkCopy or TableDirect (SQLCE) --- src/Umbraco.Core/DatabaseContext.cs | 2 +- .../Persistence/BulkDataReader.cs | 1511 ++++++++++ .../DefinitionFactory.cs | 8 +- .../Persistence/DatabaseSchemaHelper.cs | 2 +- .../Initial/DatabaseSchemaCreation.cs | 2 +- .../Persistence/PetaPocoExtensions.cs | 394 ++- .../Persistence/PocoDataDataReader.cs | 159 ++ .../Repositories/ContentRepository.cs | 6 +- .../Persistence/SqlSyntax/DbTypes.cs | 1 + .../MicrosoftSqlSyntaxProviderBase.cs | 108 + src/Umbraco.Core/Umbraco.Core.csproj | 2 + .../Persistence/BulkDataReaderTests.cs | 2432 +++++++++++++++++ .../Persistence/PetaPocoCachesTest.cs | 198 ++ .../Persistence/PetaPocoExtensionsTest.cs | 354 ++- .../MySqlSyntaxProviderTests.cs | 2 +- .../SqlCeSyntaxProviderTests.cs | 2 +- src/Umbraco.Tests/Umbraco.Tests.csproj | 2 + 17 files changed, 4901 insertions(+), 284 deletions(-) create mode 100644 src/Umbraco.Core/Persistence/BulkDataReader.cs create mode 100644 src/Umbraco.Core/Persistence/PocoDataDataReader.cs create mode 100644 src/Umbraco.Tests/Persistence/BulkDataReaderTests.cs create mode 100644 src/Umbraco.Tests/Persistence/PetaPocoCachesTest.cs diff --git a/src/Umbraco.Core/DatabaseContext.cs b/src/Umbraco.Core/DatabaseContext.cs index 05aba6b97d..585f1d51cf 100644 --- a/src/Umbraco.Core/DatabaseContext.cs +++ b/src/Umbraco.Core/DatabaseContext.cs @@ -174,7 +174,7 @@ namespace Umbraco.Core /// public void ConfigureEmbeddedDatabaseConnection() { - const string providerName = "System.Data.SqlServerCe.4.0"; + const string providerName = Constants.DatabaseProviders.SqlCe; var connectionString = GetEmbeddedDatabaseConnectionString(); SaveConnectionString(connectionString, providerName); diff --git a/src/Umbraco.Core/Persistence/BulkDataReader.cs b/src/Umbraco.Core/Persistence/BulkDataReader.cs new file mode 100644 index 0000000000..8df4dd536e --- /dev/null +++ b/src/Umbraco.Core/Persistence/BulkDataReader.cs @@ -0,0 +1,1511 @@ +using System; +using System.Collections; +using System.Collections.Generic; +using System.Collections.ObjectModel; +using System.Data; +using System.Data.Common; +using System.Data.SqlClient; +using System.Diagnostics; +using System.Globalization; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Umbraco.Core.Persistence +{ + /// + /// A base implementation of that is suitable for . + /// + /// + /// + /// Borrowed from Microsoft: + /// See: https://blogs.msdn.microsoft.com/anthonybloesch/2013/01/23/bulk-loading-data-with-idatareader-and-sqlbulkcopy/ + /// + /// This implementation is designed to be very memory efficient requiring few memory resources and to support + /// rapid transfer of data to SQL Server. + /// + /// Subclasses should implement , , + /// , , . + /// If they contain disposable resources they should override . + /// + /// SD: Alternatively, we could have used a LinqEntityDataReader which is nicer to use but it uses quite a lot of reflection and + /// I thought this would just be quicker. + /// Simple example of that: https://github.com/gridsum/DataflowEx/blob/master/Gridsum.DataflowEx/Databases/BulkDataReader.cs + /// Full example of that: https://github.com/matthewschrager/Repository/blob/master/Repository.EntityFramework/EntityDataReader.cs + /// So we know where to find that if we ever need it, these would convert any Linq data source to an IDataReader + /// + /// + internal abstract class BulkDataReader : IDataReader + { + + #region Fields + + /// + /// The containing the input row set's schema information + /// requires to function correctly. + /// + private DataTable _schemaTable = new DataTable(); + + /// + /// The mapping from the row set input to the target table's columns. + /// + private List _columnMappings = new List(); + + #endregion + + #region Subclass utility routines + + /// + /// The mapping from the row set input to the target table's columns. + /// + /// + /// If necessary, will be called to initialize the mapping. + /// + public ReadOnlyCollection ColumnMappings + { + get + { + if (this._columnMappings.Count == 0) + { + // Need to add the column definitions and mappings. + AddSchemaTableRows(); + + if (this._columnMappings.Count == 0) + { + throw new InvalidOperationException("AddSchemaTableRows did not add rows."); + } + + Debug.Assert(this._schemaTable.Rows.Count == FieldCount); + } + + return new ReadOnlyCollection(_columnMappings); + } + } + + /// + /// The name of the input row set's schema. + /// + /// + /// This may be different from the target schema but usually they are identical. + /// + protected abstract string SchemaName + { + get; + } + + /// + /// The name of the input row set's table. + /// + /// + /// This may be different from the target table but usually they are identical. + /// + protected abstract string TableName + { + get; + } + + /// + /// Adds the input row set's schema to the object. + /// + /// + /// Call + /// to do this for each row. + /// + /// + protected abstract void AddSchemaTableRows(); + + /// + /// For each , the optional columns that may have values. + /// + /// + /// This is used for checking the parameters of . + /// + /// + private static readonly Dictionary> AllowedOptionalColumnCombinations = new Dictionary> + { + { SqlDbType.BigInt, new List { } }, + { SqlDbType.Binary, new List { SchemaTableColumn.ColumnSize } }, + { SqlDbType.Bit, new List { } }, + { SqlDbType.Char, new List { SchemaTableColumn.ColumnSize } }, + { SqlDbType.Date, new List { } }, + { SqlDbType.DateTime, new List { } }, + { SqlDbType.DateTime2, new List { SchemaTableColumn.NumericPrecision } }, + { SqlDbType.DateTimeOffset, new List { SchemaTableColumn.NumericPrecision } }, + { SqlDbType.Decimal, new List { SchemaTableColumn.NumericPrecision, SchemaTableColumn.NumericScale } }, + { SqlDbType.Float, new List { SchemaTableColumn.NumericPrecision, SchemaTableColumn.NumericScale } }, + { SqlDbType.Image, new List { } }, + { SqlDbType.Int, new List { } }, + { SqlDbType.Money, new List { } }, + { SqlDbType.NChar, new List { SchemaTableColumn.ColumnSize } }, + { SqlDbType.NText, new List { } }, + { SqlDbType.NVarChar, new List { SchemaTableColumn.ColumnSize } }, + { SqlDbType.Real, new List { } }, + { SqlDbType.SmallDateTime, new List { } }, + { SqlDbType.SmallInt, new List { } }, + { SqlDbType.SmallMoney, new List { } }, + { SqlDbType.Structured, new List { } }, + { SqlDbType.Text, new List { } }, + { SqlDbType.Time, new List { SchemaTableColumn.NumericPrecision } }, + { SqlDbType.Timestamp, new List { } }, + { SqlDbType.TinyInt, new List { } }, + { SqlDbType.Udt, new List { BulkDataReader.DataTypeNameSchemaColumn } }, + { SqlDbType.UniqueIdentifier, new List { } }, + { SqlDbType.VarBinary, new List { SchemaTableColumn.ColumnSize } }, + { SqlDbType.VarChar, new List { SchemaTableColumn.ColumnSize } }, + { SqlDbType.Variant, new List { } }, + { SqlDbType.Xml, new List { BulkDataReader.XmlSchemaCollectionDatabaseSchemaColumn, BulkDataReader.XmlSchemaCollectionOwningSchemaSchemaColumn, BulkDataReader.XmlSchemaCollectionNameSchemaColumn } } + }; + + /// + /// A helper method to support . + /// + /// + /// This methds does extensive argument checks. These errors will cause hard to diagnose exceptions in latter + /// processing so it is important to detect them when they can be easily associated with the code defect. + /// + /// + /// The combination of values for the parameters is not supported. + /// + /// + /// A null value for the parameter is not supported. + /// + /// + /// The name of the column. + /// + /// + /// The size of the column which may be null if not applicable. + /// + /// + /// The precision of the column which may be null if not applicable. + /// + /// + /// The scale of the column which may be null if not applicable. + /// + /// + /// Are the column values unique (i.e. never duplicated)? + /// + /// + /// Is the column part of the primary key? + /// + /// + /// Is the column nullable (i.e. optional)? + /// + /// + /// The corresponding . + /// + /// + /// The schema name of the UDT. + /// + /// + /// The type name of the UDT. + /// + /// + /// For XML columns the schema collection's database name. Otherwise, null. + /// + /// + /// For XML columns the schema collection's schema name. Otherwise, null. + /// + /// + /// For XML columns the schema collection's name. Otherwise, null. + /// + /// + protected void AddSchemaTableRow(string columnName, + int? columnSize, + short? numericPrecision, + short? numericScale, + bool isUnique, + bool isKey, + bool allowDbNull, + SqlDbType providerType, + string udtSchema, + string udtType, + string xmlSchemaCollectionDatabase, + string xmlSchemaCollectionOwningSchema, + string xmlSchemaCollectionName) + { + if (string.IsNullOrEmpty(columnName)) + { + throw new ArgumentException("columnName must be a nonempty string."); + } + else if (columnSize.HasValue && columnSize.Value <= 0) + { + throw new ArgumentOutOfRangeException("columnSize"); + } + else if (numericPrecision.HasValue && numericPrecision.Value <= 0) + { + throw new ArgumentOutOfRangeException("numericPrecision"); + } + else if (numericScale.HasValue && numericScale.Value < 0) + { + throw new ArgumentOutOfRangeException("columnSize"); + } + + List allowedOptionalColumnList; + + if (BulkDataReader.AllowedOptionalColumnCombinations.TryGetValue(providerType, out allowedOptionalColumnList)) + { + if ((columnSize.HasValue && !allowedOptionalColumnList.Contains(SchemaTableColumn.ColumnSize)) || + (numericPrecision.HasValue && !allowedOptionalColumnList.Contains(SchemaTableColumn.NumericPrecision)) || + (numericScale.HasValue && !allowedOptionalColumnList.Contains(SchemaTableColumn.NumericScale)) || + (udtSchema != null && !allowedOptionalColumnList.Contains(BulkDataReader.DataTypeNameSchemaColumn)) || + (udtType != null && !allowedOptionalColumnList.Contains(BulkDataReader.DataTypeNameSchemaColumn)) || + (xmlSchemaCollectionDatabase != null && !allowedOptionalColumnList.Contains(BulkDataReader.XmlSchemaCollectionDatabaseSchemaColumn)) || + (xmlSchemaCollectionOwningSchema != null && !allowedOptionalColumnList.Contains(BulkDataReader.XmlSchemaCollectionOwningSchemaSchemaColumn)) || + (xmlSchemaCollectionName != null && !allowedOptionalColumnList.Contains(BulkDataReader.XmlSchemaCollectionNameSchemaColumn))) + { + throw new ArgumentException("Columns are set that are incompatible with the value of providerType."); + } + } + else + { + throw new ArgumentException("providerType is unsupported."); + } + + Type dataType; // Corresponding CLR type. + string dataTypeName; // Corresponding SQL Server type. + bool isLong = false; // Is the column a large value column (e.g. nvarchar(max))? + + switch (providerType) + { + case SqlDbType.BigInt: + dataType = typeof(long); + dataTypeName = "bigint"; + break; + + case SqlDbType.Binary: + dataType = typeof(byte[]); + + if (!columnSize.HasValue) + { + throw new ArgumentException("columnSize must be specified for \"binary\" type columns."); + } + else if (columnSize > 8000) + { + throw new ArgumentOutOfRangeException("columnSize"); + } + + dataTypeName = string.Format(CultureInfo.InvariantCulture, + "binary({0})", + columnSize.Value); + break; + + case SqlDbType.Bit: + dataType = typeof(bool); + dataTypeName = "bit"; + break; + + case SqlDbType.Char: + dataType = typeof(string); + + if (!columnSize.HasValue) + { + throw new ArgumentException("columnSize must be specified for \"char\" type columns."); + } + else if (columnSize > 8000) + { + throw new ArgumentOutOfRangeException("columnSize"); + } + + dataTypeName = string.Format(CultureInfo.InvariantCulture, + "char({0})", + columnSize.Value); + break; + + case SqlDbType.Date: + dataType = typeof(DateTime); + dataTypeName = "date"; + break; + + case SqlDbType.DateTime: + dataType = typeof(DateTime); + dataTypeName = "datetime"; + break; + + case SqlDbType.DateTime2: + dataType = typeof(DateTime); + + if (numericPrecision.HasValue) + { + if (numericPrecision.Value > 7) + { + throw new ArgumentOutOfRangeException("numericPrecision"); + } + + dataTypeName = string.Format(CultureInfo.InvariantCulture, + "datetime2({0})", + numericPrecision.Value); + } + else + { + dataTypeName = "datetime2"; + } + break; + + case SqlDbType.DateTimeOffset: + dataType = typeof(DateTimeOffset); + + if (numericPrecision.HasValue) + { + if (numericPrecision.Value > 7) + { + throw new ArgumentOutOfRangeException("numericPrecision"); + } + + dataTypeName = string.Format(CultureInfo.InvariantCulture, + "datetimeoffset({0})", + numericPrecision.Value); + } + else + { + dataTypeName = "datetimeoffset"; + } + break; + + case SqlDbType.Decimal: + dataType = typeof(decimal); + + if (!numericPrecision.HasValue || !numericScale.HasValue) + { + throw new ArgumentException("numericPrecision and numericScale must be specified for \"decimal\" type columns."); + } + else if (numericPrecision > 38) + { + throw new ArgumentOutOfRangeException("numericPrecision"); + } + else if (numericScale.Value > numericPrecision.Value) + { + throw new ArgumentException("numericScale must not be larger than numericPrecision for \"decimal\" type columns."); + } + + dataTypeName = string.Format(CultureInfo.InvariantCulture, + "decimal({0}, {1})", + numericPrecision.Value, + numericScale.Value); + break; + + case SqlDbType.Float: + dataType = typeof(double); + + if (!numericPrecision.HasValue) + { + throw new ArgumentException("numericPrecision must be specified for \"float\" type columns"); + } + else if (numericPrecision > 53) + { + throw new ArgumentOutOfRangeException("numericPrecision"); + } + + dataTypeName = string.Format(CultureInfo.InvariantCulture, + "float({0})", + numericPrecision.Value); + break; + + case SqlDbType.Image: + dataType = typeof(byte[]); + dataTypeName = "image"; + break; + + case SqlDbType.Int: + dataType = typeof(int); + dataTypeName = "int"; + break; + + case SqlDbType.Money: + dataType = typeof(decimal); + dataTypeName = "money"; + break; + + case SqlDbType.NChar: + dataType = typeof(string); + + if (!columnSize.HasValue) + { + throw new ArgumentException("columnSize must be specified for \"nchar\" type columns"); + } + else if (columnSize > 4000) + { + throw new ArgumentOutOfRangeException("columnSize"); + } + + dataTypeName = string.Format(CultureInfo.InvariantCulture, + "nchar({0})", + columnSize.Value); + break; + + case SqlDbType.NText: + dataType = typeof(string); + dataTypeName = "ntext"; + break; + + case SqlDbType.NVarChar: + dataType = typeof(string); + + if (columnSize.HasValue) + { + if (columnSize > 4000) + { + throw new ArgumentOutOfRangeException("columnSize"); + } + + dataTypeName = string.Format(CultureInfo.InvariantCulture, + "nvarchar({0})", + columnSize.Value); + } + else + { + isLong = true; + + dataTypeName = "nvarchar(max)"; + } + break; + + case SqlDbType.Real: + dataType = typeof(float); + dataTypeName = "real"; + break; + + case SqlDbType.SmallDateTime: + dataType = typeof(DateTime); + dataTypeName = "smalldatetime"; + break; + + case SqlDbType.SmallInt: + dataType = typeof(Int16); + dataTypeName = "smallint"; + break; + + case SqlDbType.SmallMoney: + dataType = typeof(decimal); + dataTypeName = "smallmoney"; + break; + + // SqlDbType.Structured not supported because it related to nested rowsets. + + case SqlDbType.Text: + dataType = typeof(string); + dataTypeName = "text"; + break; + + case SqlDbType.Time: + dataType = typeof(TimeSpan); + + if (numericPrecision.HasValue) + { + if (numericPrecision > 7) + { + throw new ArgumentOutOfRangeException("numericPrecision"); + } + + dataTypeName = string.Format(CultureInfo.InvariantCulture, + "time({0})", + numericPrecision.Value); + } + else + { + dataTypeName = "time"; + } + break; + + + // SqlDbType.Timestamp not supported because rowversions are not settable. + + case SqlDbType.TinyInt: + dataType = typeof(byte); + dataTypeName = "tinyint"; + break; + + case SqlDbType.Udt: + if (string.IsNullOrEmpty(udtSchema)) + { + throw new ArgumentException("udtSchema must be nonnull and nonempty for \"UDT\" columns."); + } + else if (string.IsNullOrEmpty(udtType)) + { + throw new ArgumentException("udtType must be nonnull and nonempty for \"UDT\" columns."); + } + + dataType = typeof(object); + using (SqlCommandBuilder commandBuilder = new SqlCommandBuilder()) + { + dataTypeName = commandBuilder.QuoteIdentifier(udtSchema) + "." + commandBuilder.QuoteIdentifier(udtType); + } + break; + + case SqlDbType.UniqueIdentifier: + dataType = typeof(Guid); + dataTypeName = "uniqueidentifier"; + break; + + case SqlDbType.VarBinary: + dataType = typeof(byte[]); + + if (columnSize.HasValue) + { + if (columnSize > 8000) + { + throw new ArgumentOutOfRangeException("columnSize"); + } + + dataTypeName = string.Format(CultureInfo.InvariantCulture, + "varbinary({0})", + columnSize.Value); + } + else + { + isLong = true; + + dataTypeName = "varbinary(max)"; + } + break; + + case SqlDbType.VarChar: + dataType = typeof(string); + + if (columnSize.HasValue) + { + if (columnSize > 8000) + { + throw new ArgumentOutOfRangeException("columnSize"); + } + + dataTypeName = string.Format(CultureInfo.InvariantCulture, + "varchar({0})", + columnSize.Value); + } + else + { + isLong = true; + + dataTypeName = "varchar(max)"; + } + break; + + case SqlDbType.Variant: + dataType = typeof(object); + dataTypeName = "sql_variant"; + break; + + case SqlDbType.Xml: + dataType = typeof(string); + + if (xmlSchemaCollectionName == null) + { + if (xmlSchemaCollectionDatabase != null || xmlSchemaCollectionOwningSchema != null) + { + throw new ArgumentException("xmlSchemaCollectionDatabase and xmlSchemaCollectionOwningSchema must be null if xmlSchemaCollectionName is null for \"xml\" columns."); + } + + dataTypeName = "xml"; + } + else + { + if (xmlSchemaCollectionName.Length == 0) + { + throw new ArgumentException("xmlSchemaCollectionName must be nonempty or null for \"xml\" columns."); + } + else if (xmlSchemaCollectionDatabase != null && + xmlSchemaCollectionDatabase.Length == 0) + { + throw new ArgumentException("xmlSchemaCollectionDatabase must be null or nonempty for \"xml\" columns."); + } + else if (xmlSchemaCollectionOwningSchema != null && + xmlSchemaCollectionOwningSchema.Length == 0) + { + throw new ArgumentException("xmlSchemaCollectionOwningSchema must be null or nonempty for \"xml\" columns."); + } + + System.Text.StringBuilder schemaCollection = new System.Text.StringBuilder("xml("); + + if (xmlSchemaCollectionDatabase != null) + { + schemaCollection.Append("[" + xmlSchemaCollectionDatabase + "]"); + } + + schemaCollection.Append("[" + (xmlSchemaCollectionOwningSchema == null ? SchemaName : xmlSchemaCollectionOwningSchema) + "]"); + schemaCollection.Append("[" + xmlSchemaCollectionName + "]"); + + dataTypeName = schemaCollection.ToString(); + } + break; + + default: + throw new ArgumentOutOfRangeException("providerType"); + + } + + this._schemaTable.Rows.Add(columnName, + _schemaTable.Rows.Count, + columnSize, + numericPrecision, + numericScale, + isUnique, + isKey, + "TraceServer", + "TraceWarehouse", + columnName, + SchemaName, + TableName, + dataType, + allowDbNull, + providerType, + false, // isAliased + false, // isExpression + false, // isIdentity, + false, // isAutoIncrement, + false, // isRowVersion, + false, // isHidden, + isLong, + true, // isReadOnly, + dataType, + dataTypeName, + xmlSchemaCollectionDatabase, + xmlSchemaCollectionOwningSchema, + xmlSchemaCollectionName); + + this._columnMappings.Add(new SqlBulkCopyColumnMapping(columnName, columnName)); + } + + #endregion + + #region Constructors + + private const string IsIdentitySchemaColumn = "IsIdentity"; + + private const string DataTypeNameSchemaColumn = "DataTypeName"; + + private const string XmlSchemaCollectionDatabaseSchemaColumn = "XmlSchemaCollectionDatabase"; + + private const string XmlSchemaCollectionOwningSchemaSchemaColumn = "XmlSchemaCollectionOwningSchema"; + + private const string XmlSchemaCollectionNameSchemaColumn = "XmlSchemaCollectionName"; + + /// + /// Constructor. + /// + protected BulkDataReader() + { + this._schemaTable.Locale = System.Globalization.CultureInfo.InvariantCulture; + + DataColumnCollection columns = _schemaTable.Columns; + + columns.Add(SchemaTableColumn.ColumnName, typeof(System.String)); + columns.Add(SchemaTableColumn.ColumnOrdinal, typeof(System.Int32)); + columns.Add(SchemaTableColumn.ColumnSize, typeof(System.Int32)); + columns.Add(SchemaTableColumn.NumericPrecision, typeof(System.Int16)); + columns.Add(SchemaTableColumn.NumericScale, typeof(System.Int16)); + columns.Add(SchemaTableColumn.IsUnique, typeof(System.Boolean)); + columns.Add(SchemaTableColumn.IsKey, typeof(System.Boolean)); + columns.Add(SchemaTableOptionalColumn.BaseServerName, typeof(System.String)); + columns.Add(SchemaTableOptionalColumn.BaseCatalogName, typeof(System.String)); + columns.Add(SchemaTableColumn.BaseColumnName, typeof(System.String)); + columns.Add(SchemaTableColumn.BaseSchemaName, typeof(System.String)); + columns.Add(SchemaTableColumn.BaseTableName, typeof(System.String)); + columns.Add(SchemaTableColumn.DataType, typeof(System.Type)); + columns.Add(SchemaTableColumn.AllowDBNull, typeof(System.Boolean)); + columns.Add(SchemaTableColumn.ProviderType, typeof(System.Int32)); + columns.Add(SchemaTableColumn.IsAliased, typeof(System.Boolean)); + columns.Add(SchemaTableColumn.IsExpression, typeof(System.Boolean)); + columns.Add(BulkDataReader.IsIdentitySchemaColumn, typeof(System.Boolean)); + columns.Add(SchemaTableOptionalColumn.IsAutoIncrement, typeof(System.Boolean)); + columns.Add(SchemaTableOptionalColumn.IsRowVersion, typeof(System.Boolean)); + columns.Add(SchemaTableOptionalColumn.IsHidden, typeof(System.Boolean)); + columns.Add(SchemaTableColumn.IsLong, typeof(System.Boolean)); + columns.Add(SchemaTableOptionalColumn.IsReadOnly, typeof(System.Boolean)); + columns.Add(SchemaTableOptionalColumn.ProviderSpecificDataType, typeof(System.Type)); + columns.Add(BulkDataReader.DataTypeNameSchemaColumn, typeof(System.String)); + columns.Add(BulkDataReader.XmlSchemaCollectionDatabaseSchemaColumn, typeof(System.String)); + columns.Add(BulkDataReader.XmlSchemaCollectionOwningSchemaSchemaColumn, typeof(System.String)); + columns.Add(BulkDataReader.XmlSchemaCollectionNameSchemaColumn, typeof(System.String)); + } + + #endregion + + #region IDataReader + + /// + /// Gets a value indicating the depth of nesting for the current row. (Inherited from .) + /// + /// + /// does not support nested result sets so this method always returns 0. + /// + /// + public int Depth + { + get { return 0; } + } + + /// + /// Gets the number of columns in the current row. (Inherited from .) + /// + /// + public int FieldCount + { + get { return GetSchemaTable().Rows.Count; } + } + + /// + /// Is the bulk copy process open? + /// + bool _isOpen = true; + + /// + /// Gets a value indicating whether the data reader is closed. (Inherited from .) + /// + /// + public bool IsClosed + { + get { return !_isOpen; } + } + + /// + /// Gets the column located at the specified index. (Inherited from .) + /// + /// + /// No column with the specified index was found. + /// + /// + /// The zero-based index of the column to get. + /// + /// + /// The column located at the specified index as an . + /// + /// + public object this[int i] + { + get { return GetValue(i); } + } + + /// + /// Gets the column with the specified name. (Inherited from .) + /// + /// + /// No column with the specified name was found. + /// + /// + /// The name of the column to find. + /// + /// + /// The column located at the specified name as an . + /// + /// + public object this[string name] + { + get { return GetValue(GetOrdinal(name)); } + } + + /// + /// Gets the number of rows changed, inserted, or deleted by execution of the SQL statement. (Inherited from .) + /// + /// + /// Always returns -1 which is the expected behaviour for statements. + /// + /// + public virtual int RecordsAffected + { + get { return -1; } + } + + /// + /// Closes the . (Inherited from .) + /// + /// + public void Close() + { + this._isOpen = false; + } + + /// + /// Gets the value of the specified column as a . (Inherited from .) + /// + /// + /// The index passed was outside the range of 0 through . + /// + /// + /// The zero-based column ordinal. + /// + /// + /// The value of the column. + /// + /// + public bool GetBoolean(int i) + { + return (bool)GetValue(i); + } + + /// + /// Gets the value of the specified column as a . (Inherited from .) + /// + /// + /// The index passed was outside the range of 0 through . + /// + /// + /// The zero-based column ordinal. + /// + /// + /// The value of the column. + /// + /// + public byte GetByte(int i) + { + return (byte)GetValue(i); + } + + /// + /// Reads a stream of bytes from the specified column offset into the buffer as an array, starting at the given buffer offset. + /// (Inherited from .) + /// + /// + /// If you pass a buffer that is null, returns the length of the row in bytes. + /// + /// + /// The index passed was outside the range of 0 through . + /// + /// + /// The zero-based column ordinal. + /// + /// + /// The index within the field from which to start the read operation. + /// + /// + /// The buffer into which to read the stream of bytes. + /// + /// + /// The index for buffer to start the read operation. + /// + /// + /// The number of bytes to read. + /// + /// + /// The actual number of bytes read. + /// + /// + public long GetBytes(int i, + long fieldOffset, + byte[] buffer, + int bufferoffset, + int length) + { + byte[] data = (byte[])GetValue(i); + + if (buffer != null) + { + Array.Copy(data, fieldOffset, buffer, bufferoffset, length); + } + + return data.LongLength; + } + + /// + /// Gets the value of the specified column as a . (Inherited from .) + /// + /// + /// The index passed was outside the range of 0 through . + /// + /// + /// The zero-based column ordinal. + /// + /// + /// The value of the column. + /// + /// + public char GetChar(int i) + { + char result; + + object data = GetValue(i); + char? dataAsChar = data as char?; + char[] dataAsCharArray = data as char[]; + string dataAsString = data as string; + + if (dataAsChar.HasValue) + { + result = dataAsChar.Value; + } + else if (dataAsCharArray != null && + dataAsCharArray.Length == 1) + { + result = dataAsCharArray[0]; + } + else if (dataAsString != null && + dataAsString.Length == 1) + { + result = dataAsString[0]; + } + else + { + throw new InvalidOperationException("GetValue did not return a Char compatible type."); + } + + return result; + } + + /// + /// Reads a stream of characters from the specified column offset into the buffer as an array, starting at the given buffer offset. + /// (Inherited from .) + /// + /// + /// If you pass a buffer that is null, returns the length of the row in bytes. + /// + /// + /// The index passed was outside the range of 0 through . + /// + /// + /// The zero-based column ordinal. + /// + /// + /// The index within the field from which to start the read operation. + /// + /// + /// The buffer into which to read the stream of characters. + /// + /// + /// The index for buffer to start the read operation. + /// + /// + /// The number of characters to read. + /// + /// + /// The actual number of characters read. + /// + /// + public long GetChars(int i, + long fieldoffset, + char[] buffer, + int bufferoffset, + int length) + { + object data = GetValue(i); + + string dataAsString = data as string; + char[] dataAsCharArray = data as char[]; + + if (dataAsString != null) + { + dataAsCharArray = dataAsString.ToCharArray((int)fieldoffset, length); + } + else if (dataAsCharArray == null) + { + throw new InvalidOperationException("GetValue did not return either a Char array or a String."); + } + + if (buffer != null) + { + Array.Copy(dataAsCharArray, fieldoffset, buffer, bufferoffset, length); + } + + return dataAsCharArray.LongLength; + } + + /// + /// Returns an IDataReader for the specified column ordinal. (Inherited from .) + /// + /// + /// does not support nested result sets so this method always returns null. + /// + /// + /// The index passed was outside the range of 0 through . + /// + /// + /// The zero-based column ordinal. + /// + /// + /// The for the specified column ordinal (null). + /// + /// + public IDataReader GetData(int i) + { + if (i < 0 || i >= this.FieldCount) + { + throw new ArgumentOutOfRangeException("i"); + } + + return null; + } + + /// + /// The data type information for the specified field. (Inherited from .) + /// + /// + /// The index passed was outside the range of 0 through . + /// + /// + /// The zero-based column ordinal. + /// + /// + /// The data type information for the specified field. + /// + /// + public string GetDataTypeName(int i) + { + return GetFieldType(i).Name; + } + + /// + /// Gets the value of the specified column as a . (Inherited from .) + /// + /// + /// The index passed was outside the range of 0 through . + /// + /// + /// The zero-based column ordinal. + /// + /// + /// The value of the column. + /// + /// + public DateTime GetDateTime(int i) + { + return (DateTime)GetValue(i); + } + + /// + /// Gets the value of the specified column as a . + /// + /// + /// The index passed was outside the range of 0 through . + /// + /// + /// The zero-based column ordinal. + /// + /// + /// The value of the column. + /// + public DateTimeOffset GetDateTimeOffset(int i) + { + return (DateTimeOffset)GetValue(i); + } + + /// + /// Gets the value of the specified column as a . (Inherited from .) + /// + /// + /// The index passed was outside the range of 0 through . + /// + /// + /// The zero-based column ordinal. + /// + /// + /// The value of the column. + /// + /// + public decimal GetDecimal(int i) + { + return (Decimal)GetValue(i); + } + + /// + /// Gets the value of the specified column as a . (Inherited from .) + /// + /// + /// The index passed was outside the range of 0 through . + /// + /// + /// The zero-based column ordinal. + /// + /// + /// The value of the column. + /// + /// + public double GetDouble(int i) + { + return (double)GetValue(i); + } + + /// + /// Gets the information corresponding to the type of that would be returned from . + /// (Inherited from .) + /// + /// + /// The index passed was outside the range of 0 through . + /// + /// + /// The zero-based column ordinal. + /// + /// + /// The information corresponding to the type of that would be returned from . + /// + /// + public Type GetFieldType(int i) + { + return (Type)GetSchemaTable().Rows[i][SchemaTableColumn.DataType]; + } + + /// + /// Gets the value of the specified column as a . (Inherited from .) + /// + /// + /// The index passed was outside the range of 0 through . + /// + /// + /// The zero-based column ordinal. + /// + /// + /// The value of the column. + /// + /// + public float GetFloat(int i) + { + return (float)this[i]; + } + + /// + /// Gets the value of the specified column as a . (Inherited from .) + /// + /// + /// The index passed was outside the range of 0 through . + /// + /// + /// The zero-based column ordinal. + /// + /// + /// The value of the column. + /// + /// + public Guid GetGuid(int i) + { + return (Guid)GetValue(i); + } + + /// + /// Gets the value of the specified column as a . (Inherited from .) + /// + /// + /// The index passed was outside the range of 0 through . + /// + /// + /// The zero-based column ordinal. + /// + /// + /// The value of the column. + /// + /// + public short GetInt16(int i) + { + return (short)GetValue(i); + } + + /// + /// Gets the value of the specified column as a . (Inherited from .) + /// + /// + /// The index passed was outside the range of 0 through . + /// + /// + /// The zero-based column ordinal. + /// + /// + /// The value of the column. + /// + /// + public int GetInt32(int i) + { + return (int)GetValue(i); + } + + /// + /// Gets the value of the specified column as a . (Inherited from .) + /// + /// + /// The index passed was outside the range of 0 through . + /// + /// + /// The zero-based column ordinal. + /// + /// + /// The value of the column. + /// + /// + public long GetInt64(int i) + { + return (long)GetValue(i); + } + + /// + /// Gets the name for the field to find. (Inherited from .) + /// + /// + /// The index passed was outside the range of 0 through . + /// + /// + /// The zero-based column ordinal. + /// + /// + /// The name of the field or the empty string (""), if there is no value to return. + /// + /// + public string GetName(int i) + { + return (string)GetSchemaTable().Rows[i][SchemaTableColumn.ColumnName]; + } + + /// + /// Return the index of the named field. (Inherited from .) + /// + /// + /// The index of the named field was not found. + /// + /// + /// The name of the field to find. + /// + /// + /// The index of the named field. + /// + /// + public int GetOrdinal(string name) + { + if (name == null) // Empty strings are handled as a IndexOutOfRangeException. + { + throw new ArgumentNullException("name"); + } + + int result = -1; + + int rowCount = FieldCount; + + DataRowCollection schemaRows = GetSchemaTable().Rows; + + // Case sensitive search + for (int ordinal = 0; ordinal < rowCount; ordinal++) + { + if (String.Equals((string)schemaRows[ordinal][SchemaTableColumn.ColumnName], name, StringComparison.Ordinal)) + { + result = ordinal; + } + } + + if (result == -1) + { + // Case insensitive search. + for (int ordinal = 0; ordinal < rowCount; ordinal++) + { + if (String.Equals((string)schemaRows[ordinal][SchemaTableColumn.ColumnName], name, StringComparison.OrdinalIgnoreCase)) + { + result = ordinal; + } + } + } + + if (result == -1) + { + throw new IndexOutOfRangeException(name); + } + + return result; + } + + /// + /// Returns a that describes the column metadata of the . (Inherited from .) + /// + /// + /// The is closed. + /// + /// + /// A that describes the column metadata. + /// + /// + public DataTable GetSchemaTable() + { + if (IsClosed) + { + throw new InvalidOperationException("The IDataReader is closed."); + } + + if (_schemaTable.Rows.Count == 0) + { + // Need to add the column definitions and mappings + _schemaTable.TableName = TableName; + + AddSchemaTableRows(); + + Debug.Assert(_schemaTable.Rows.Count == FieldCount); + } + + return _schemaTable; + } + + /// + /// Gets the value of the specified column as a . (Inherited from .) + /// + /// + /// The index passed was outside the range of 0 through . + /// + /// + /// The zero-based column ordinal. + /// + /// + /// The value of the column. + /// + /// + public string GetString(int i) + { + return (string)GetValue(i); + } + + /// + /// Gets the value of the specified column as a . + /// + /// + /// The index passed was outside the range of 0 through . + /// + /// + /// The zero-based column ordinal. + /// + /// + /// The value of the column. + /// + public TimeSpan GetTimeSpan(int i) + { + return (TimeSpan)GetValue(i); + } + + /// + /// Gets the value of the specified column as a . (Inherited from .) + /// + /// + /// The index passed was outside the range of 0 through . + /// + /// + /// The zero-based column ordinal. + /// + /// + /// The value of the column. + /// + /// + public abstract object GetValue(int i); + + /// + /// Populates an array of objects with the column values of the current record. (Inherited from .) + /// + /// + /// was null. + /// + /// + /// An array of to copy the attribute fields into. + /// + /// + /// The number of instances of in the array. + /// + /// + public int GetValues(object[] values) + { + if (values == null) + { + throw new ArgumentNullException("values"); + } + + int fieldCount = Math.Min(FieldCount, values.Length); + + for (int i = 0; i < fieldCount; i++) + { + values[i] = GetValue(i); + } + + return fieldCount; + } + + /// + /// Return whether the specified field is set to null. (Inherited from .) + /// + /// + /// The index passed was outside the range of 0 through . + /// + /// + /// The zero-based column ordinal. + /// + /// + /// True if the specified field is set to null; otherwise, false. + /// + /// + public bool IsDBNull(int i) + { + object data = GetValue(i); + + return data == null || Convert.IsDBNull(data); + } + + /// + /// Advances the data reader to the next result, when reading the results of batch SQL statements. (Inherited from .) + /// + /// + /// for returns a single result set so false is always returned. + /// + /// + /// True if there are more rows; otherwise, false. for returns a single result set so false is always returned. + /// + /// + public bool NextResult() + { + return false; + } + + /// + /// Advances the to the next record. (Inherited from .) + /// + /// + /// True if there are more rows; otherwise, false. + /// + /// + public abstract bool Read(); + + #endregion + + #region IDisposable + + /// + /// Has the object been disposed? + /// + bool _disposed = false; + + /// + /// Dispose of any disposable and expensive resources. + /// + /// + /// Is this call the result of a call? + /// + protected virtual void Dispose(bool disposing) + { + if (!this._disposed) + { + this._disposed = true; + + if (disposing) + { + if (_schemaTable != null) + { + _schemaTable.Dispose(); + this._schemaTable = null; + } + + this._columnMappings = null; + + this._isOpen = false; + + GC.SuppressFinalize(this); + } + } + } + + /// + /// Performs application-defined tasks associated with freeing, releasing, or resetting unmanaged resources. (Inherited from .) + /// + /// + public void Dispose() + { + Dispose(true); + } + + /// + /// Finalizer + /// + /// + /// has no unmanaged resources but a subclass may thus a finalizer is required. + /// + ~BulkDataReader() + { + Dispose(false); + } + + #endregion + + } +} diff --git a/src/Umbraco.Core/Persistence/DatabaseModelDefinitions/DefinitionFactory.cs b/src/Umbraco.Core/Persistence/DatabaseModelDefinitions/DefinitionFactory.cs index 9b08279716..e3c35e01b4 100644 --- a/src/Umbraco.Core/Persistence/DatabaseModelDefinitions/DefinitionFactory.cs +++ b/src/Umbraco.Core/Persistence/DatabaseModelDefinitions/DefinitionFactory.cs @@ -9,7 +9,7 @@ namespace Umbraco.Core.Persistence.DatabaseModelDefinitions { internal static class DefinitionFactory { - public static TableDefinition GetTableDefinition(Type modelType) + public static TableDefinition GetTableDefinition(ISqlSyntaxProvider syntaxProvider, Type modelType) { //Looks for PetaPoco's TableNameAtribute for the name of the table //If no attribute is set we use the name of the Type as the default convention @@ -32,7 +32,7 @@ namespace Umbraco.Core.Persistence.DatabaseModelDefinitions //Otherwise use the name of the property itself as the default convention var columnAttribute = propertyInfo.FirstAttribute(); string columnName = columnAttribute != null ? columnAttribute.Name : propertyInfo.Name; - var columnDefinition = GetColumnDefinition(modelType, propertyInfo, columnName, tableName); + var columnDefinition = GetColumnDefinition(syntaxProvider, modelType, propertyInfo, columnName, tableName); tableDefinition.Columns.Add(columnDefinition); //Creates a foreignkey definition and adds it to the collection on the table definition @@ -58,7 +58,7 @@ namespace Umbraco.Core.Persistence.DatabaseModelDefinitions return tableDefinition; } - public static ColumnDefinition GetColumnDefinition(Type modelType, PropertyInfo propertyInfo, string columnName, string tableName) + public static ColumnDefinition GetColumnDefinition(ISqlSyntaxProvider syntaxProvider, Type modelType, PropertyInfo propertyInfo, string columnName, string tableName) { var definition = new ColumnDefinition{ Name = columnName, TableName = tableName, ModificationType = ModificationType.Create }; @@ -110,7 +110,7 @@ namespace Umbraco.Core.Persistence.DatabaseModelDefinitions { //Special case for MySQL as it can't have multiple default DateTime values, which //is what the umbracoServer table definition is trying to create - if (SqlSyntaxContext.SqlSyntaxProvider is MySqlSyntaxProvider && definition.TableName == "umbracoServer" && + if (syntaxProvider is MySqlSyntaxProvider && definition.TableName == "umbracoServer" && definition.TableName.ToLowerInvariant() == "lastNotifiedDate".ToLowerInvariant()) return definition; diff --git a/src/Umbraco.Core/Persistence/DatabaseSchemaHelper.cs b/src/Umbraco.Core/Persistence/DatabaseSchemaHelper.cs index bcc1528d3f..944d5a8a74 100644 --- a/src/Umbraco.Core/Persistence/DatabaseSchemaHelper.cs +++ b/src/Umbraco.Core/Persistence/DatabaseSchemaHelper.cs @@ -99,7 +99,7 @@ namespace Umbraco.Core.Persistence public void CreateTable(bool overwrite, Type modelType) { - var tableDefinition = DefinitionFactory.GetTableDefinition(modelType); + var tableDefinition = DefinitionFactory.GetTableDefinition(_syntaxProvider, modelType); var tableName = tableDefinition.Name; string createSql = _syntaxProvider.Format(tableDefinition); diff --git a/src/Umbraco.Core/Persistence/Migrations/Initial/DatabaseSchemaCreation.cs b/src/Umbraco.Core/Persistence/Migrations/Initial/DatabaseSchemaCreation.cs index 423c847c47..ab477953b4 100644 --- a/src/Umbraco.Core/Persistence/Migrations/Initial/DatabaseSchemaCreation.cs +++ b/src/Umbraco.Core/Persistence/Migrations/Initial/DatabaseSchemaCreation.cs @@ -159,7 +159,7 @@ namespace Umbraco.Core.Persistence.Migrations.Initial foreach (var item in OrderedTables.OrderBy(x => x.Key)) { - var tableDefinition = DefinitionFactory.GetTableDefinition(item.Value); + var tableDefinition = DefinitionFactory.GetTableDefinition(_sqlSyntaxProvider, item.Value); result.TableDefinitions.Add(tableDefinition); } diff --git a/src/Umbraco.Core/Persistence/PetaPocoExtensions.cs b/src/Umbraco.Core/Persistence/PetaPocoExtensions.cs index 610b3d9b02..d1667456e6 100644 --- a/src/Umbraco.Core/Persistence/PetaPocoExtensions.cs +++ b/src/Umbraco.Core/Persistence/PetaPocoExtensions.cs @@ -3,9 +3,13 @@ using System.Collections.Generic; using System.Data; using System.Data.Common; using System.Data.SqlClient; +using System.Data.SqlServerCe; using System.Linq; using System.Text.RegularExpressions; +using MySql.Data.MySqlClient; +using StackExchange.Profiling.Data; using Umbraco.Core.Logging; +using Umbraco.Core.Persistence.DatabaseModelDefinitions; using Umbraco.Core.Persistence.Querying; using Umbraco.Core.Persistence.SqlSyntax; @@ -150,7 +154,7 @@ namespace Umbraco.Core.Persistence { //this fancy regex will only match a single @ not a double, etc... var regex = new Regex("(?(overwrite); } + /// + /// Performs the bulk insertion in the context of a current transaction with an optional parameter to complete the transaction + /// when finished + /// + /// + /// + /// public static void BulkInsertRecords(this Database db, IEnumerable collection) { //don't do anything if there are no records. @@ -180,10 +191,97 @@ namespace Umbraco.Core.Persistence using (var tr = db.GetTransaction()) { - db.BulkInsertRecords(collection, tr, true); + db.BulkInsertRecords(collection, tr, SqlSyntaxContext.SqlSyntaxProvider, true); } } + /// + /// Performs the bulk insertion in the context of a current transaction with an optional parameter to complete the transaction + /// when finished + /// + /// + /// + /// + /// + /// + /// + /// If this is false this will try to just generate bulk insert statements instead of using the current SQL platform's bulk + /// insert logic. For SQLCE, bulk insert statements do not work so if this is false it will insert one at a time. + /// + /// + /// The number of items inserted + public static int BulkInsertRecords(this Database db, + IEnumerable collection, + Transaction tr, + ISqlSyntaxProvider syntaxProvider, + bool useNativeSqlPlatformBulkInsert = true, + bool commitTrans = false) + { + + //don't do anything if there are no records. + if (collection.Any() == false) + { + return 0; + } + + var pd = Database.PocoData.ForType(typeof(T)); + if (pd == null) throw new InvalidOperationException("Could not find PocoData for " + typeof(T)); + + try + { + int processed = 0; + + var usedNativeSqlPlatformInserts = useNativeSqlPlatformBulkInsert + && NativeSqlPlatformBulkInsertRecords(db, syntaxProvider, pd, collection, out processed); + + if (usedNativeSqlPlatformInserts == false) + { + //if it is sql ce or it is a sql server version less than 2008, we need to do individual inserts. + var sqlServerSyntax = syntaxProvider as SqlServerSyntaxProvider; + if ((sqlServerSyntax != null && (int) sqlServerSyntax.GetVersionName(db) < (int) SqlServerVersionName.V2008) + || syntaxProvider is SqlCeSyntaxProvider) + { + //SqlCe doesn't support bulk insert statements! + foreach (var poco in collection) + { + db.Insert(poco); + } + } + else + { + //we'll need to generate insert statements instead + + string[] sqlStatements; + var cmds = db.GenerateBulkInsertCommand(pd, collection, out sqlStatements); + for (var i = 0; i < sqlStatements.Length; i++) + { + using (var cmd = cmds[i]) + { + cmd.CommandText = sqlStatements[i]; + cmd.ExecuteNonQuery(); + processed++; + } + } + } + } + + if (commitTrans) + { + tr.Complete(); + } + return processed; + } + catch + { + if (commitTrans) + { + tr.Dispose(); + } + throw; + } + + } + /// /// Performs the bulk insertion in the context of a current transaction with an optional parameter to complete the transaction /// when finished @@ -193,64 +291,10 @@ namespace Umbraco.Core.Persistence /// /// /// + [Obsolete("Use the method that specifies an SqlSyntaxContext instance instead")] public static void BulkInsertRecords(this Database db, IEnumerable collection, Transaction tr, bool commitTrans = false) { - //TODO: We should change this to use BulkCopy, as an example see: - // https://ayende.com/blog/4137/nhibernate-perf-tricks - // Even though this just generates lots of raw sql INSERT statements BulkCopy is the fastest it can possibly be - // and we should be able to do this using the current connection from the PetaPoco db instance (and would probably be much cleaner) - // - // BulkCopy is available for SQL Server and MySqlBulkLoader is available for MySql, pretty sure BulkCopy works for SQLCE so - // we should be covered and of course could fallback to this method if that is not our database. But we would get huge perf - // increases for this. - - - //don't do anything if there are no records. - if (collection.Any() == false) - return; - - try - { - //if it is sql ce or it is a sql server version less than 2008, we need to do individual inserts. - var sqlServerSyntax = SqlSyntaxContext.SqlSyntaxProvider as SqlServerSyntaxProvider; - - if ((sqlServerSyntax != null && (int)sqlServerSyntax.GetVersionName(db) < (int)SqlServerVersionName.V2008) - || SqlSyntaxContext.SqlSyntaxProvider is SqlCeSyntaxProvider) - { - //SqlCe doesn't support bulk insert statements! - - foreach (var poco in collection) - { - db.Insert(poco); - } - } - else - { - string[] sqlStatements; - var cmds = db.GenerateBulkInsertCommand(collection, db.Connection, out sqlStatements); - for (var i = 0; i < sqlStatements.Length; i++) - { - using (var cmd = cmds[i]) - { - cmd.CommandText = sqlStatements[i]; - cmd.ExecuteNonQuery(); - } - } - } - - if (commitTrans) - { - tr.Complete(); - } - } - catch - { - if (commitTrans) - { - tr.Dispose(); - } - throw; - } + db.BulkInsertRecords(collection, tr, SqlSyntaxContext.SqlSyntaxProvider, commitTrans); } /// @@ -259,8 +303,8 @@ namespace Umbraco.Core.Persistence /// /// /// - /// /// + /// /// Sql commands with populated command parameters required to execute the sql statement /// /// The limits for number of parameters are 2100 (in sql server, I think there's many more allowed in mysql). So @@ -269,33 +313,24 @@ namespace Umbraco.Core.Persistence /// that is max. I've reduced it to 2000 anyways. /// internal static IDbCommand[] GenerateBulkInsertCommand( - this Database db, - IEnumerable collection, - IDbConnection connection, + this Database db, + Database.PocoData pd, + IEnumerable collection, out string[] sql) { - //A filter used below a few times to get all columns except result cols and not the primary key if it is auto-incremental - Func, bool> includeColumn = (data, column) => - { - if (column.Value.ResultColumn) return false; - if (data.TableInfo.AutoIncrement && column.Key == data.TableInfo.PrimaryKey) return false; - return true; - }; - - var pd = Database.PocoData.ForType(typeof(T)); var tableName = db.EscapeTableName(pd.TableInfo.TableName); //get all columns to include and format for sql - var cols = string.Join(", ", + var cols = string.Join(", ", pd.Columns - .Where(c => includeColumn(pd, c)) + .Where(c => IncludeColumn(pd, c)) .Select(c => tableName + "." + db.EscapeSqlIdentifier(c.Key)).ToArray()); var itemArray = collection.ToArray(); //calculate number of parameters per item - var paramsPerItem = pd.Columns.Count(i => includeColumn(pd, i)); - + var paramsPerItem = pd.Columns.Count(i => IncludeColumn(pd, i)); + //Example calc: // Given: we have 4168 items in the itemArray, each item contains 8 command parameters (values to be inserterted) // 2100 / 8 = 262.5 @@ -316,14 +351,14 @@ namespace Umbraco.Core.Persistence .Skip(tIndex * (int)itemsPerTrans) .Take((int)itemsPerTrans); - var cmd = db.CreateCommand(connection, ""); + var cmd = db.CreateCommand(db.Connection, string.Empty); var pocoValues = new List(); var index = 0; foreach (var poco in itemsForTrans) { var values = new List(); //get all columns except result cols and not the primary key if it is auto-incremental - foreach (var i in pd.Columns.Where(x => includeColumn(pd, x))) + foreach (var i in pd.Columns.Where(x => IncludeColumn(pd, x))) { db.AddParam(cmd, i.Value.GetValue(poco), "@"); values.Add(string.Format("{0}{1}", "@", index++)); @@ -331,14 +366,211 @@ namespace Umbraco.Core.Persistence pocoValues.Add("(" + string.Join(",", values.ToArray()) + ")"); } - var sqlResult = string.Format("INSERT INTO {0} ({1}) VALUES {2}", tableName, cols, string.Join(", ", pocoValues)); + var sqlResult = string.Format("INSERT INTO {0} ({1}) VALUES {2}", tableName, cols, string.Join(", ", pocoValues)); sqlQueries.Add(sqlResult); commands.Add(cmd); } sql = sqlQueries.ToArray(); - return commands.ToArray(); + return commands.ToArray(); + } + + /// + /// A filter used below a few times to get all columns except result cols and not the primary key if it is auto-incremental + /// + /// + /// + /// + private static bool IncludeColumn(Database.PocoData data, KeyValuePair column) + { + if (column.Value.ResultColumn) return false; + if (data.TableInfo.AutoIncrement && column.Key == data.TableInfo.PrimaryKey) return false; + return true; + } + + /// + /// Bulk insert records with Sql BulkCopy or TableDirect or whatever sql platform specific bulk insert records should be used + /// + /// + /// + /// + /// + /// The number of records inserted + private static bool NativeSqlPlatformBulkInsertRecords(Database db, ISqlSyntaxProvider syntaxProvider, Database.PocoData pd, IEnumerable collection, out int processed) + { + + var dbConnection = db.Connection; + + //unwrap the profiled connection if there is one + var profiledConnection = dbConnection as ProfiledDbConnection; + if (profiledConnection != null) + { + dbConnection = profiledConnection.InnerConnection; + } + + //check if it's SQL or SqlCe + + var sqlConnection = dbConnection as SqlConnection; + if (sqlConnection != null) + { + processed = BulkInsertRecordsSqlServer(db, (SqlServerSyntaxProvider)syntaxProvider, pd, collection); + return true; + } + + var sqlCeConnection = dbConnection as SqlCeConnection; + if (sqlCeConnection != null) + { + processed = BulkInsertRecordsSqlCe(db, pd, collection); + return true; + } + + //could not use the SQL server's specific bulk insert operations + processed = 0; + return false; + + } + + /// + /// Logic used to perform bulk inserts with SqlCe's TableDirect + /// + /// + /// + /// + /// + /// + internal static int BulkInsertRecordsSqlCe(Database db, + Database.PocoData pd, + IEnumerable collection) + { + var cols = pd.Columns.ToArray(); + + using (var cmd = db.CreateCommand(db.Connection, string.Empty)) + { + cmd.CommandText = pd.TableInfo.TableName; + cmd.CommandType = CommandType.TableDirect; + //cmd.Transaction = GetTypedTransaction(db.Connection.); + + //get the real command + using (var sqlCeCommand = GetTypedCommand(cmd)) + { + // This seems to cause problems, I think this is primarily used for retrieval, not + // inserting. see: https://msdn.microsoft.com/en-us/library/system.data.sqlserverce.sqlcecommand.indexname%28v=vs.100%29.aspx?f=255&MSPPError=-2147217396 + //sqlCeCommand.IndexName = pd.TableInfo.PrimaryKey; + + var count = 0; + using (var rs = sqlCeCommand.ExecuteResultSet(ResultSetOptions.Updatable)) + { + var rec = rs.CreateRecord(); + + foreach (var item in collection) + { + for (var i = 0; i < cols.Length; i++) + { + //skip the index if this shouldn't be included (i.e. PK) + if (IncludeColumn(pd, cols[i])) + { + var val = cols[i].Value.GetValue(item); + rec.SetValue(i, val); + } + } + rs.Insert(rec); + count++; + } + } + return count; + } + + } + } + + /// + /// Logic used to perform bulk inserts with SqlServer's BulkCopy + /// + /// + /// + /// + /// + /// + /// + internal static int BulkInsertRecordsSqlServer(Database db, SqlServerSyntaxProvider sqlSyntaxProvider, + Database.PocoData pd, IEnumerable collection) + { + //NOTE: We need to use the original db.Connection here to create the command, but we need to pass in the typed + // connection below to the SqlBulkCopy + using (var cmd = db.CreateCommand(db.Connection, string.Empty)) + { + using (var copy = new SqlBulkCopy( + GetTypedConnection(db.Connection), + SqlBulkCopyOptions.Default, + GetTypedTransaction(cmd.Transaction)) + { + BulkCopyTimeout = 10000, + DestinationTableName = pd.TableInfo.TableName + }) + { + //var cols = pd.Columns.Where(x => IncludeColumn(pd, x)).Select(x => x.Value).ToArray(); + + using (var bulkReader = new PocoDataDataReader(collection, pd, sqlSyntaxProvider)) + { + copy.WriteToServer(bulkReader); + + return bulkReader.RecordsAffected; + } + } + } + } + + + /// + /// Returns the underlying connection as a typed connection - this is used to unwrap the profiled mini profiler stuff + /// + /// + /// + /// + private static TConnection GetTypedConnection(IDbConnection connection) + where TConnection : class, IDbConnection + { + var profiled = connection as ProfiledDbConnection; + if (profiled != null) + { + return profiled.InnerConnection as TConnection; + } + return connection as TConnection; + } + + /// + /// Returns the underlying connection as a typed connection - this is used to unwrap the profiled mini profiler stuff + /// + /// + /// + /// + private static TTransaction GetTypedTransaction(IDbTransaction connection) + where TTransaction : class, IDbTransaction + { + var profiled = connection as ProfiledDbTransaction; + if (profiled != null) + { + return profiled.WrappedTransaction as TTransaction; + } + return connection as TTransaction; + } + + /// + /// Returns the underlying connection as a typed connection - this is used to unwrap the profiled mini profiler stuff + /// + /// + /// + /// + private static TCommand GetTypedCommand(IDbCommand command) + where TCommand : class, IDbCommand + { + var profiled = command as ProfiledDbCommand; + if (profiled != null) + { + return profiled.InternalCommand as TCommand; + } + return command as TCommand; } [Obsolete("Use the DatabaseSchemaHelper instead")] @@ -415,8 +647,8 @@ namespace Umbraco.Core.Persistence return ApplicationContext.Current.DatabaseContext.DatabaseProvider; } - + } - + } \ No newline at end of file diff --git a/src/Umbraco.Core/Persistence/PocoDataDataReader.cs b/src/Umbraco.Core/Persistence/PocoDataDataReader.cs new file mode 100644 index 0000000000..b0479a311a --- /dev/null +++ b/src/Umbraco.Core/Persistence/PocoDataDataReader.cs @@ -0,0 +1,159 @@ +using System; +using System.Collections.Generic; +using System.Data; +using System.Linq; +using Umbraco.Core.Persistence.DatabaseAnnotations; +using Umbraco.Core.Persistence.DatabaseModelDefinitions; +using Umbraco.Core.Persistence.SqlSyntax; + +namespace Umbraco.Core.Persistence +{ + /// + /// A data reader used for reading collections of PocoData entity types + /// + /// + /// We are using a custom data reader so that tons of memory is not consumed when rebuilding this table, previously + /// we'd generate SQL insert statements, but we'd have to put all of the XML structures into memory first. Alternatively + /// we can use .net's DataTable, but this also requires putting everything into memory. By using a DataReader we don't have to + /// store every content item and it's XML structure in memory to get it into the DB, we can stream it into the db with this + /// reader. + /// + internal class PocoDataDataReader : BulkDataReader + where TSyntax : ISqlSyntaxProvider + { + private readonly MicrosoftSqlSyntaxProviderBase _sqlSyntaxProvider; + private readonly TableDefinition _tableDefinition; + private readonly Database.PocoColumn[] _readerColumns; + private readonly IEnumerator _enumerator; + private readonly ColumnDefinition[] _columnDefinitions; + private int _recordsAffected = -1; + + public PocoDataDataReader( + IEnumerable dataSource, + Database.PocoData pd, + MicrosoftSqlSyntaxProviderBase sqlSyntaxProvider) + { + if (dataSource == null) throw new ArgumentNullException("dataSource"); + if (sqlSyntaxProvider == null) throw new ArgumentNullException("sqlSyntaxProvider"); + + _tableDefinition = DefinitionFactory.GetTableDefinition(sqlSyntaxProvider, pd.type); + if (_tableDefinition == null) throw new InvalidOperationException("No table definition found for type " + pd.type); + + _readerColumns = pd.Columns.Select(x => x.Value).ToArray(); + _sqlSyntaxProvider = sqlSyntaxProvider; + _enumerator = dataSource.GetEnumerator(); + _columnDefinitions = _tableDefinition.Columns.ToArray(); + + } + + protected override string SchemaName + { + get { return _tableDefinition.SchemaName; } + } + + protected override string TableName + { + get { return _tableDefinition.Name; } + } + + public override int RecordsAffected + { + get { return _recordsAffected <= 0 ? -1 : _recordsAffected; } + } + + /// + /// This will automatically add the schema rows based on the Poco table definition and the columns passed in + /// + protected override void AddSchemaTableRows() + { + //var colNames = _readerColumns.Select(x => x.ColumnName).ToArray(); + //foreach (var col in _columnDefinitions.Where(x => colNames.Contains(x.Name, StringComparer.OrdinalIgnoreCase))) + foreach (var col in _columnDefinitions) + { + var sqlDbType = SqlDbType.NVarChar; + if (col.HasSpecialDbType) + { + //get the SqlDbType from the 'special type' + switch (col.DbType) + { + case SpecialDbTypes.NTEXT: + sqlDbType = SqlDbType.NText; + break; + case SpecialDbTypes.NCHAR: + sqlDbType = SqlDbType.NChar; + break; + default: + throw new ArgumentOutOfRangeException(); + } + } + else if (col.Type.HasValue) + { + //get the SqlDbType from the DbType + sqlDbType = _sqlSyntaxProvider.GetSqlDbType(col.Type.Value); + } + else + { + //get the SqlDbType from the clr type + sqlDbType = _sqlSyntaxProvider.GetSqlDbType(col.PropertyType); + } + + AddSchemaTableRow( + col.Name, + col.Size > 0 ? (int?)col.Size : null, + col.Precision > 0 ? (short?)col.Precision : null, + null, col.IsUnique, col.IsIdentity, col.IsNullable, sqlDbType, + null, null, null, null, null); + } + + } + + /// + /// Get the value from the column index for the current object + /// + /// + /// + public override object GetValue(int i) + { + if (_enumerator.Current != null) + { + return _readerColumns[i].GetValue(_enumerator.Current); + //return _columnDefinitions[i]. .GetValue(_enumerator.Current); + } + + return null; + //TODO: Or throw ? + } + + /// + /// Advance the cursor + /// + /// + public override bool Read() + { + var result = _enumerator.MoveNext(); + if (result) + { + if (_recordsAffected == -1) + { + _recordsAffected = 0; + } + _recordsAffected++; + } + return result; + } + + /// + /// Ensure the enumerator is disposed + /// + /// + protected override void Dispose(bool disposing) + { + base.Dispose(disposing); + + if (disposing) + { + _enumerator.Dispose(); + } + } + } +} \ No newline at end of file diff --git a/src/Umbraco.Core/Persistence/Repositories/ContentRepository.cs b/src/Umbraco.Core/Persistence/Repositories/ContentRepository.cs index 9222769247..9827382b00 100644 --- a/src/Umbraco.Core/Persistence/Repositories/ContentRepository.cs +++ b/src/Umbraco.Core/Persistence/Repositories/ContentRepository.cs @@ -248,12 +248,12 @@ namespace Umbraco.Core.Persistence.Repositories var xmlItems = (from descendant in descendants let xml = serializer(descendant) - select new ContentXmlDto { NodeId = descendant.Id, Xml = xml.ToDataString() }).ToArray(); + select new ContentXmlDto { NodeId = descendant.Id, Xml = xml.ToDataString() }); //bulk insert it into the database - Database.BulkInsertRecords(xmlItems, tr); + var count = Database.BulkInsertRecords(xmlItems, tr, SqlSyntax); - processed += xmlItems.Length; + processed += count; pageIndex++; } while (processed < total); diff --git a/src/Umbraco.Core/Persistence/SqlSyntax/DbTypes.cs b/src/Umbraco.Core/Persistence/SqlSyntax/DbTypes.cs index 507db230cc..c9ef4d1f35 100644 --- a/src/Umbraco.Core/Persistence/SqlSyntax/DbTypes.cs +++ b/src/Umbraco.Core/Persistence/SqlSyntax/DbTypes.cs @@ -4,6 +4,7 @@ using System.Data; namespace Umbraco.Core.Persistence.SqlSyntax { + //TODO: TSyntax should be removed, it's not used/needed here public class DbTypes where TSyntax : ISqlSyntaxProvider { diff --git a/src/Umbraco.Core/Persistence/SqlSyntax/MicrosoftSqlSyntaxProviderBase.cs b/src/Umbraco.Core/Persistence/SqlSyntax/MicrosoftSqlSyntaxProviderBase.cs index 449f5fb3b1..1ea600b6e4 100644 --- a/src/Umbraco.Core/Persistence/SqlSyntax/MicrosoftSqlSyntaxProviderBase.cs +++ b/src/Umbraco.Core/Persistence/SqlSyntax/MicrosoftSqlSyntaxProviderBase.cs @@ -1,4 +1,6 @@ using System; +using System.Data; +using System.Linq; using Umbraco.Core.Persistence.Querying; namespace Umbraco.Core.Persistence.SqlSyntax @@ -133,5 +135,111 @@ namespace Umbraco.Core.Persistence.SqlSyntax throw new ArgumentOutOfRangeException("columnType"); } } + + /// + /// This uses a the DbTypeMap created and custom mapping to resolve the SqlDbType + /// + /// + /// + public virtual SqlDbType GetSqlDbType(Type clrType) + { + var dbType = DbTypeMap.ColumnDbTypeMap.First(x => x.Key == clrType).Value; + return GetSqlDbType(dbType); + } + + /// + /// Returns the mapped SqlDbType for the DbType specified + /// + /// + /// + public virtual SqlDbType GetSqlDbType(DbType dbType) + { + var sqlDbType = SqlDbType.NVarChar; + + //SEE: https://msdn.microsoft.com/en-us/library/cc716729(v=vs.110).aspx + // and https://msdn.microsoft.com/en-us/library/yy6y35y8%28v=vs.110%29.aspx?f=255&MSPPError=-2147217396 + switch (dbType) + { + case DbType.AnsiString: + sqlDbType = SqlDbType.VarChar; + break; + case DbType.Binary: + sqlDbType = SqlDbType.VarBinary; + break; + case DbType.Byte: + sqlDbType = SqlDbType.TinyInt; + break; + case DbType.Boolean: + sqlDbType = SqlDbType.Bit; + break; + case DbType.Currency: + sqlDbType = SqlDbType.Money; + break; + case DbType.Date: + sqlDbType = SqlDbType.Date; + break; + case DbType.DateTime: + sqlDbType = SqlDbType.DateTime; + break; + case DbType.Decimal: + sqlDbType = SqlDbType.Decimal; + break; + case DbType.Double: + sqlDbType = SqlDbType.Float; + break; + case DbType.Guid: + sqlDbType = SqlDbType.UniqueIdentifier; + break; + case DbType.Int16: + sqlDbType = SqlDbType.SmallInt; + break; + case DbType.Int32: + sqlDbType = SqlDbType.Int; + break; + case DbType.Int64: + sqlDbType = SqlDbType.BigInt; + break; + case DbType.Object: + sqlDbType = SqlDbType.Variant; + break; + case DbType.SByte: + throw new NotSupportedException("Inferring a SqlDbType from SByte is not supported."); + case DbType.Single: + sqlDbType = SqlDbType.Real; + break; + case DbType.String: + sqlDbType = SqlDbType.NVarChar; + break; + case DbType.Time: + sqlDbType = SqlDbType.Time; + break; + case DbType.UInt16: + throw new NotSupportedException("Inferring a SqlDbType from UInt16 is not supported."); + case DbType.UInt32: + throw new NotSupportedException("Inferring a SqlDbType from UInt32 is not supported."); + case DbType.UInt64: + throw new NotSupportedException("Inferring a SqlDbType from UInt64 is not supported."); + case DbType.VarNumeric: + throw new NotSupportedException("Inferring a VarNumeric from UInt64 is not supported."); + case DbType.AnsiStringFixedLength: + sqlDbType = SqlDbType.Char; + break; + case DbType.StringFixedLength: + sqlDbType = SqlDbType.NChar; + break; + case DbType.Xml: + sqlDbType = SqlDbType.Xml; + break; + case DbType.DateTime2: + sqlDbType = SqlDbType.DateTime2; + break; + case DbType.DateTimeOffset: + sqlDbType = SqlDbType.DateTimeOffset; + break; + default: + throw new ArgumentOutOfRangeException(); + } + return sqlDbType; + } } } \ No newline at end of file diff --git a/src/Umbraco.Core/Umbraco.Core.csproj b/src/Umbraco.Core/Umbraco.Core.csproj index bf70f6afd6..c28cea24cd 100644 --- a/src/Umbraco.Core/Umbraco.Core.csproj +++ b/src/Umbraco.Core/Umbraco.Core.csproj @@ -415,6 +415,7 @@ + @@ -472,6 +473,7 @@ + diff --git a/src/Umbraco.Tests/Persistence/BulkDataReaderTests.cs b/src/Umbraco.Tests/Persistence/BulkDataReaderTests.cs new file mode 100644 index 0000000000..b1e1a79ddb --- /dev/null +++ b/src/Umbraco.Tests/Persistence/BulkDataReaderTests.cs @@ -0,0 +1,2432 @@ +using System; +using System.Collections.Generic; +using System.Collections.ObjectModel; +using System.Data; +using System.Data.Common; +using System.Data.SqlClient; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using NUnit.Framework; +using Umbraco.Core.Persistence; + +namespace Umbraco.Tests.Persistence +{ + /// + /// Unit tests for . + /// + /// + /// Borrowed from Microsoft: + /// See: https://blogs.msdn.microsoft.com/anthonybloesch/2013/01/23/bulk-loading-data-with-idatareader-and-sqlbulkcopy/ + /// + [TestFixture] + public class BulkDataReaderTest + { + + #region Test constants + + /// + /// The schema name. + /// + private const string testSchemaName = "TestSchema"; + + /// + /// The table name. + /// + private const string testTableName = "TestTable"; + + /// + /// The test UDT schema name. + /// + private const string testUdtSchemaName = "UdtSchema"; + + /// + /// The test UDT name. + /// + private const string testUdtName = "TestUdt"; + + /// + /// The test XML schema collection database name. + /// + private const string testXmlSchemaCollectionDatabaseName = "XmlDatabase"; + + /// + /// The test XML schema collection owning schema name. + /// + private const string testXMLSchemaCollectionSchemaName = "XmlSchema"; + + /// + /// The test XML schema collection name. + /// + private const string testXMLSchemaCollectionName = "Xml"; + + #endregion + + #region Schema tests + + /// + /// Test that is functioning correctly. + /// + /// + [Test] + public void ColumnMappingsTest() + { + using (BulkDataReaderSubclass testReader = new BulkDataReaderSubclass()) + { + ReadOnlyCollection columnMappings = testReader.ColumnMappings; + + Assert.IsTrue(columnMappings.Count > 0); + Assert.AreEqual(columnMappings.Count, testReader.FieldCount); + + foreach (SqlBulkCopyColumnMapping columnMapping in columnMappings) + { + Assert.AreEqual(columnMapping.SourceColumn, columnMapping.DestinationColumn); + } + } + } + + /// + /// Test that is functioning correctly. + /// + /// + [Test] + public void GetDataTypeNameTest() + { + using (BulkDataReaderSubclass testReader = new BulkDataReaderSubclass()) + { + Assert.IsTrue(testReader.FieldCount > 0); + + for (int currentColumn = 0; currentColumn < testReader.FieldCount; currentColumn++) + { + Assert.AreEqual(testReader.GetDataTypeName(currentColumn), ((Type)testReader.GetSchemaTable().Rows[currentColumn][SchemaTableColumn.DataType]).Name); + } + } + } + + /// + /// Test that is functioning correctly. + /// + /// + [Test] + public void GetFieldTypeTest() + { + using (BulkDataReaderSubclass testReader = new BulkDataReaderSubclass()) + { + Assert.IsTrue(testReader.FieldCount > 0); + + for (int currentColumn = 0; currentColumn < testReader.FieldCount; currentColumn++) + { + Assert.AreEqual(testReader.GetFieldType(currentColumn), testReader.GetSchemaTable().Rows[currentColumn][SchemaTableColumn.DataType]); + } + } + } + + /// + /// Test that is functioning correctly. + /// + /// + [Test] + public void GetOrdinalTest() + { + using (BulkDataReaderSubclass testReader = new BulkDataReaderSubclass()) + { + Assert.IsTrue(testReader.FieldCount > 0); + + for (int currentColumn = 0; currentColumn < testReader.FieldCount; currentColumn++) + { + Assert.AreEqual(testReader.GetOrdinal(testReader.GetName(currentColumn)), currentColumn); + + Assert.AreEqual(testReader.GetOrdinal(testReader.GetName(currentColumn).ToUpperInvariant()), currentColumn); + } + } + } + + /// + /// Test that functions correctly. + /// + /// + /// uses to test legal schema combinations. + /// + /// + [Test] + public void GetSchemaTableTest() + { + using (BulkDataReaderSubclass testReader = new BulkDataReaderSubclass()) + { + DataTable schemaTable = testReader.GetSchemaTable(); + + Assert.IsNotNull(schemaTable); + Assert.IsTrue(schemaTable.Rows.Count > 0); + Assert.AreEqual(schemaTable.Rows.Count, BulkDataReaderSubclass.ExpectedResultSet.Count); + } + } + + /// + /// Test that + /// throws a for null column names. + /// + /// + /// Uses to test the illegal schema combination. + /// + /// + [Test] + [ExpectedException(typeof(ArgumentException))] + public void AddSchemaTableRowNullColumnNameTest() + { + using (BulkDataReaderSchemaTest testReader = new BulkDataReaderSchemaTest()) + { + testReader.AllowDBNull = false; + testReader.ColumnName = null; + testReader.ColumnSize = null; + testReader.IsKey = false; + testReader.IsUnique = false; + testReader.NumericPrecision = null; + testReader.NumericScale = null; + testReader.ProviderType = SqlDbType.BigInt; + testReader.UdtSchema = null; + testReader.UdtType = null; + testReader.XmlSchemaCollectionDatabase = null; + testReader.XmlSchemaCollectionOwningSchema = null; + testReader.XmlSchemaCollectionName = null; + + DataTable schemaTable = testReader.GetSchemaTable(); ; + } + } + + /// + /// Test that + /// throws a for empty column names. + /// + /// + /// Uses to test the illegal schema combination. + /// + /// + [Test] + [ExpectedException(typeof(ArgumentException))] + public void AddSchemaTableRowEmptyColumnNameTest() + { + using (BulkDataReaderSchemaTest testReader = new BulkDataReaderSchemaTest()) + { + testReader.AllowDBNull = false; + testReader.ColumnName = string.Empty; + testReader.ColumnSize = null; + testReader.IsKey = false; + testReader.IsUnique = false; + testReader.NumericPrecision = null; + testReader.NumericScale = null; + testReader.ProviderType = SqlDbType.BigInt; + testReader.UdtSchema = null; + testReader.UdtType = null; + testReader.XmlSchemaCollectionDatabase = null; + testReader.XmlSchemaCollectionOwningSchema = null; + testReader.XmlSchemaCollectionName = null; + + DataTable schemaTable = testReader.GetSchemaTable(); ; + } + } + + /// + /// Test that + /// throws a for nonpositive column sizes. + /// + /// + /// Uses to test the illegal schema combination. + /// + /// + [Test] + [ExpectedException(typeof(ArgumentOutOfRangeException))] + public void AddSchemaTableRowNonpositiveColumnSizeTest() + { + using (BulkDataReaderSchemaTest testReader = new BulkDataReaderSchemaTest()) + { + testReader.AllowDBNull = false; + testReader.ColumnName = "Name"; + testReader.ColumnSize = 0; + testReader.IsKey = false; + testReader.IsUnique = false; + testReader.NumericPrecision = null; + testReader.NumericScale = null; + testReader.ProviderType = SqlDbType.NVarChar; + testReader.UdtSchema = null; + testReader.UdtType = null; + testReader.XmlSchemaCollectionDatabase = null; + testReader.XmlSchemaCollectionOwningSchema = null; + testReader.XmlSchemaCollectionName = null; + + DataTable schemaTable = testReader.GetSchemaTable(); ; + } + } + + /// + /// Test that + /// throws a for nonpositive numeric precision. + /// + /// + /// Uses to test the illegal schema combination. + /// + /// + [Test] + [ExpectedException(typeof(ArgumentOutOfRangeException))] + public void AddSchemaTableRowNonpositiveNumericPrecisionTest() + { + using (BulkDataReaderSchemaTest testReader = new BulkDataReaderSchemaTest()) + { + testReader.AllowDBNull = false; + testReader.ColumnName = "Name"; + testReader.ColumnSize = null; + testReader.IsKey = false; + testReader.IsUnique = false; + testReader.NumericPrecision = 0; + testReader.NumericScale = 0; + testReader.ProviderType = SqlDbType.Decimal; + testReader.UdtSchema = null; + testReader.UdtType = null; + testReader.XmlSchemaCollectionDatabase = null; + testReader.XmlSchemaCollectionOwningSchema = null; + testReader.XmlSchemaCollectionName = null; + + DataTable schemaTable = testReader.GetSchemaTable(); ; + } + } + + /// + /// Test that + /// throws a for negative numeric scale. + /// + /// + /// Uses to test the illegal schema combination. + /// + /// + [Test] + [ExpectedException(typeof(ArgumentOutOfRangeException))] + public void AddSchemaTableRowNegativeNumericScaleTest() + { + using (BulkDataReaderSchemaTest testReader = new BulkDataReaderSchemaTest()) + { + testReader.AllowDBNull = false; + testReader.ColumnName = "Name"; + testReader.ColumnSize = null; + testReader.IsKey = false; + testReader.IsUnique = false; + testReader.NumericPrecision = 5; + testReader.NumericScale = -1; + testReader.ProviderType = SqlDbType.Decimal; + testReader.UdtSchema = null; + testReader.UdtType = null; + testReader.XmlSchemaCollectionDatabase = null; + testReader.XmlSchemaCollectionOwningSchema = null; + testReader.XmlSchemaCollectionName = null; + + DataTable schemaTable = testReader.GetSchemaTable(); ; + } + } + + /// + /// Test that + /// throws a for binary column without a column size. + /// + /// + /// Uses to test the illegal schema combination. + /// + /// + [Test] + [ExpectedException(typeof(ArgumentException))] + public void AddSchemaTableRowBinaryWithoutSizeTest() + { + using (BulkDataReaderSchemaTest testReader = new BulkDataReaderSchemaTest()) + { + testReader.AllowDBNull = false; + testReader.ColumnName = "Name"; + testReader.ColumnSize = null; + testReader.IsKey = false; + testReader.IsUnique = false; + testReader.NumericPrecision = null; + testReader.NumericScale = null; + testReader.ProviderType = SqlDbType.Binary; + testReader.UdtSchema = null; + testReader.UdtType = null; + testReader.XmlSchemaCollectionDatabase = null; + testReader.XmlSchemaCollectionOwningSchema = null; + testReader.XmlSchemaCollectionName = null; + + DataTable schemaTable = testReader.GetSchemaTable(); ; + } + } + + /// + /// Test that + /// throws a for binary column with a column size that is too large (>8000). + /// + /// + /// Uses to test the illegal schema combination. + /// + /// + [Test] + [ExpectedException(typeof(ArgumentOutOfRangeException))] + public void AddSchemaTableRowBinaryWithTooLargeSizeTest() + { + using (BulkDataReaderSchemaTest testReader = new BulkDataReaderSchemaTest()) + { + testReader.AllowDBNull = false; + testReader.ColumnName = "Name"; + testReader.ColumnSize = 8001; + testReader.IsKey = false; + testReader.IsUnique = false; + testReader.NumericPrecision = null; + testReader.NumericScale = null; + testReader.ProviderType = SqlDbType.Binary; + testReader.UdtSchema = null; + testReader.UdtType = null; + testReader.XmlSchemaCollectionDatabase = null; + testReader.XmlSchemaCollectionOwningSchema = null; + testReader.XmlSchemaCollectionName = null; + + DataTable schemaTable = testReader.GetSchemaTable(); ; + } + } + + /// + /// Test that + /// throws a for char column without a column size. + /// + /// + /// Uses to test the illegal schema combination. + /// + /// + [Test] + [ExpectedException(typeof(ArgumentException))] + public void AddSchemaTableRowCharWithoutSizeTest() + { + using (BulkDataReaderSchemaTest testReader = new BulkDataReaderSchemaTest()) + { + testReader.AllowDBNull = false; + testReader.ColumnName = "Name"; + testReader.ColumnSize = null; + testReader.IsKey = false; + testReader.IsUnique = false; + testReader.NumericPrecision = null; + testReader.NumericScale = null; + testReader.ProviderType = SqlDbType.Char; + testReader.UdtSchema = null; + testReader.UdtType = null; + testReader.XmlSchemaCollectionDatabase = null; + testReader.XmlSchemaCollectionOwningSchema = null; + testReader.XmlSchemaCollectionName = null; + + DataTable schemaTable = testReader.GetSchemaTable(); ; + } + } + + /// + /// Test that + /// throws a for char column with a column size that is too large (>8000). + /// + /// + /// Uses to test the illegal schema combination. + /// + /// + [Test] + [ExpectedException(typeof(ArgumentOutOfRangeException))] + public void AddSchemaTableRowCharWithTooLargeSizeTest() + { + using (BulkDataReaderSchemaTest testReader = new BulkDataReaderSchemaTest()) + { + testReader.AllowDBNull = false; + testReader.ColumnName = "Name"; + testReader.ColumnSize = 8001; + testReader.IsKey = false; + testReader.IsUnique = false; + testReader.NumericPrecision = null; + testReader.NumericScale = null; + testReader.ProviderType = SqlDbType.Char; + testReader.UdtSchema = null; + testReader.UdtType = null; + testReader.XmlSchemaCollectionDatabase = null; + testReader.XmlSchemaCollectionOwningSchema = null; + testReader.XmlSchemaCollectionName = null; + + DataTable schemaTable = testReader.GetSchemaTable(); ; + } + } + + /// + /// Test that + /// throws a for decimal column without a column precision. + /// + /// + /// Uses to test the illegal schema combination. + /// + /// + [Test] + [ExpectedException(typeof(ArgumentException))] + public void AddSchemaTableRowDecimalWithoutPrecisionTest() + { + using (BulkDataReaderSchemaTest testReader = new BulkDataReaderSchemaTest()) + { + testReader.AllowDBNull = false; + testReader.ColumnName = "Name"; + testReader.ColumnSize = null; + testReader.IsKey = false; + testReader.IsUnique = false; + testReader.NumericPrecision = null; + testReader.NumericScale = 5; + testReader.ProviderType = SqlDbType.Decimal; + testReader.UdtSchema = null; + testReader.UdtType = null; + testReader.XmlSchemaCollectionDatabase = null; + testReader.XmlSchemaCollectionOwningSchema = null; + testReader.XmlSchemaCollectionName = null; + + DataTable schemaTable = testReader.GetSchemaTable(); ; + } + } + + /// + /// Test that + /// throws a for decimal column without a column scale. + /// + /// + /// Uses to test the illegal schema combination. + /// + /// + [Test] + [ExpectedException(typeof(ArgumentException))] + public void AddSchemaTableRowDecimalWithoutScaleTest() + { + using (BulkDataReaderSchemaTest testReader = new BulkDataReaderSchemaTest()) + { + testReader.AllowDBNull = false; + testReader.ColumnName = "Name"; + testReader.ColumnSize = null; + testReader.IsKey = false; + testReader.IsUnique = false; + testReader.NumericPrecision = 20; + testReader.NumericScale = null; + testReader.ProviderType = SqlDbType.Decimal; + testReader.UdtSchema = null; + testReader.UdtType = null; + testReader.XmlSchemaCollectionDatabase = null; + testReader.XmlSchemaCollectionOwningSchema = null; + testReader.XmlSchemaCollectionName = null; + + DataTable schemaTable = testReader.GetSchemaTable(); ; + } + } + + /// + /// Test that + /// throws a for decimal column with a column precision that is too large (>38). + /// + /// + /// Uses to test the illegal schema combination. + /// + /// + [Test] + [ExpectedException(typeof(ArgumentOutOfRangeException))] + public void AddSchemaTableRowDecimalWithTooLargePrecisionTest() + { + using (BulkDataReaderSchemaTest testReader = new BulkDataReaderSchemaTest()) + { + testReader.AllowDBNull = false; + testReader.ColumnName = "Name"; + testReader.ColumnSize = null; + testReader.IsKey = false; + testReader.IsUnique = false; + testReader.NumericPrecision = 39; + testReader.NumericScale = 5; + testReader.ProviderType = SqlDbType.Decimal; + testReader.UdtSchema = null; + testReader.UdtType = null; + testReader.XmlSchemaCollectionDatabase = null; + testReader.XmlSchemaCollectionOwningSchema = null; + testReader.XmlSchemaCollectionName = null; + + DataTable schemaTable = testReader.GetSchemaTable(); ; + } + } + + /// + /// Test that + /// throws a for decimal column with a column scale that is larger than the column precision. + /// + /// + /// Uses to test the illegal schema combination. + /// + /// + [Test] + [ExpectedException(typeof(ArgumentException))] + public void AddSchemaTableRowDecimalWithTooLargeScaleTest() + { + using (BulkDataReaderSchemaTest testReader = new BulkDataReaderSchemaTest()) + { + testReader.AllowDBNull = false; + testReader.ColumnName = "Name"; + testReader.ColumnSize = null; + testReader.IsKey = false; + testReader.IsUnique = false; + testReader.NumericPrecision = 20; + testReader.NumericScale = 21; + testReader.ProviderType = SqlDbType.Decimal; + testReader.UdtSchema = null; + testReader.UdtType = null; + testReader.XmlSchemaCollectionDatabase = null; + testReader.XmlSchemaCollectionOwningSchema = null; + testReader.XmlSchemaCollectionName = null; + + DataTable schemaTable = testReader.GetSchemaTable(); ; + } + } + + /// + /// Test that + /// throws a for datetime2 column with a column size that has a precision that is too large (>7). + /// + /// + /// Uses to test the illegal schema combination. + /// + /// + [Test] + [ExpectedException(typeof(ArgumentOutOfRangeException))] + public void AddSchemaTableRowDateTime2WithTooLargePrecisionTest() + { + using (BulkDataReaderSchemaTest testReader = new BulkDataReaderSchemaTest()) + { + testReader.AllowDBNull = false; + testReader.ColumnName = "Name"; + testReader.ColumnSize = null; + testReader.IsKey = false; + testReader.IsUnique = false; + testReader.NumericPrecision = 8; + testReader.NumericScale = null; + testReader.ProviderType = SqlDbType.DateTime2; + testReader.UdtSchema = null; + testReader.UdtType = null; + testReader.XmlSchemaCollectionDatabase = null; + testReader.XmlSchemaCollectionOwningSchema = null; + testReader.XmlSchemaCollectionName = null; + + DataTable schemaTable = testReader.GetSchemaTable(); ; + } + } + + /// + /// Test that + /// throws a for datetimeoffset column with a column size that has a precision that is too large (>7). + /// + /// + /// Uses to test the illegal schema combination. + /// + /// + [Test] + [ExpectedException(typeof(ArgumentOutOfRangeException))] + public void AddSchemaTableRowDateTimeOffsetWithTooLargePrecisionTest() + { + using (BulkDataReaderSchemaTest testReader = new BulkDataReaderSchemaTest()) + { + testReader.AllowDBNull = false; + testReader.ColumnName = "Name"; + testReader.ColumnSize = null; + testReader.IsKey = false; + testReader.IsUnique = false; + testReader.NumericPrecision = 8; + testReader.NumericScale = null; + testReader.ProviderType = SqlDbType.DateTimeOffset; + testReader.UdtSchema = null; + testReader.UdtType = null; + testReader.XmlSchemaCollectionDatabase = null; + testReader.XmlSchemaCollectionOwningSchema = null; + testReader.XmlSchemaCollectionName = null; + + DataTable schemaTable = testReader.GetSchemaTable(); ; + } + } + + /// + /// Test that + /// throws a for nchar column without a precision. + /// + /// + /// Uses to test the illegal schema combination. + /// + /// + [Test] + [ExpectedException(typeof(ArgumentException))] + public void AddSchemaTableRowFloatWithoutPrecisionTest() + { + using (BulkDataReaderSchemaTest testReader = new BulkDataReaderSchemaTest()) + { + testReader.AllowDBNull = false; + testReader.ColumnName = "Name"; + testReader.ColumnSize = null; + testReader.IsKey = false; + testReader.IsUnique = false; + testReader.NumericPrecision = null; + testReader.NumericScale = null; + testReader.ProviderType = SqlDbType.Float; + testReader.UdtSchema = null; + testReader.UdtType = null; + testReader.XmlSchemaCollectionDatabase = null; + testReader.XmlSchemaCollectionOwningSchema = null; + testReader.XmlSchemaCollectionName = null; + + DataTable schemaTable = testReader.GetSchemaTable(); ; + } + } + + /// + /// Test that + /// throws a for float column with a column precision that is too large (>53). + /// + /// + /// Uses to test the illegal schema combination. + /// + /// + [Test] + [ExpectedException(typeof(ArgumentOutOfRangeException))] + public void AddSchemaTableRowFloatWithTooLargePrecisionTest() + { + using (BulkDataReaderSchemaTest testReader = new BulkDataReaderSchemaTest()) + { + testReader.AllowDBNull = false; + testReader.ColumnName = "Name"; + testReader.ColumnSize = null; + testReader.IsKey = false; + testReader.IsUnique = false; + testReader.NumericPrecision = 54; + testReader.NumericScale = null; + testReader.ProviderType = SqlDbType.Float; + testReader.UdtSchema = null; + testReader.UdtType = null; + testReader.XmlSchemaCollectionDatabase = null; + testReader.XmlSchemaCollectionOwningSchema = null; + testReader.XmlSchemaCollectionName = null; + + DataTable schemaTable = testReader.GetSchemaTable(); ; + } + } + + /// + /// Test that + /// throws a for nchar column without a column size. + /// + /// + /// Uses to test the illegal schema combination. + /// + /// + [Test] + [ExpectedException(typeof(ArgumentException))] + public void AddSchemaTableRowNCharWithoutSizeTest() + { + using (BulkDataReaderSchemaTest testReader = new BulkDataReaderSchemaTest()) + { + testReader.AllowDBNull = false; + testReader.ColumnName = "Name"; + testReader.ColumnSize = null; + testReader.IsKey = false; + testReader.IsUnique = false; + testReader.NumericPrecision = null; + testReader.NumericScale = null; + testReader.ProviderType = SqlDbType.NChar; + testReader.UdtSchema = null; + testReader.UdtType = null; + testReader.XmlSchemaCollectionDatabase = null; + testReader.XmlSchemaCollectionOwningSchema = null; + testReader.XmlSchemaCollectionName = null; + + DataTable schemaTable = testReader.GetSchemaTable(); ; + } + } + + /// + /// Test that + /// throws a for nchar column with a column size that is too large (>4000). + /// + /// + /// Uses to test the illegal schema combination. + /// + /// + [Test] + [ExpectedException(typeof(ArgumentOutOfRangeException))] + public void AddSchemaTableRowNCharWithTooLargeSizeTest() + { + using (BulkDataReaderSchemaTest testReader = new BulkDataReaderSchemaTest()) + { + testReader.AllowDBNull = false; + testReader.ColumnName = "Name"; + testReader.ColumnSize = 4001; + testReader.IsKey = false; + testReader.IsUnique = false; + testReader.NumericPrecision = null; + testReader.NumericScale = null; + testReader.ProviderType = SqlDbType.NChar; + testReader.UdtSchema = null; + testReader.UdtType = null; + testReader.XmlSchemaCollectionDatabase = null; + testReader.XmlSchemaCollectionOwningSchema = null; + testReader.XmlSchemaCollectionName = null; + + DataTable schemaTable = testReader.GetSchemaTable(); ; + } + } + + /// + /// Test that + /// throws a for nvarchar column with a column size that is too large (>4000). + /// + /// + /// Uses to test the illegal schema combination. + /// + /// + [Test] + [ExpectedException(typeof(ArgumentOutOfRangeException))] + public void AddSchemaTableRowNVarCharWithTooLargeSizeTest() + { + using (BulkDataReaderSchemaTest testReader = new BulkDataReaderSchemaTest()) + { + testReader.AllowDBNull = false; + testReader.ColumnName = "Name"; + testReader.ColumnSize = 4001; + testReader.IsKey = false; + testReader.IsUnique = false; + testReader.NumericPrecision = null; + testReader.NumericScale = null; + testReader.ProviderType = SqlDbType.NVarChar; + testReader.UdtSchema = null; + testReader.UdtType = null; + testReader.XmlSchemaCollectionDatabase = null; + testReader.XmlSchemaCollectionOwningSchema = null; + testReader.XmlSchemaCollectionName = null; + + DataTable schemaTable = testReader.GetSchemaTable(); ; + } + } + + /// + /// Test that + /// throws a for time column with a column precision that is too large (>7). + /// + /// + /// Uses to test the illegal schema combination. + /// + /// + [Test] + [ExpectedException(typeof(ArgumentOutOfRangeException))] + public void AddSchemaTableRowTimeWithTooLargePrecisionTest() + { + using (BulkDataReaderSchemaTest testReader = new BulkDataReaderSchemaTest()) + { + testReader.AllowDBNull = false; + testReader.ColumnName = "Name"; + testReader.ColumnSize = null; + testReader.IsKey = false; + testReader.IsUnique = false; + testReader.NumericPrecision = 8; + testReader.NumericScale = null; + testReader.ProviderType = SqlDbType.Time; + testReader.UdtSchema = null; + testReader.UdtType = null; + testReader.XmlSchemaCollectionDatabase = null; + testReader.XmlSchemaCollectionOwningSchema = null; + testReader.XmlSchemaCollectionName = null; + + DataTable schemaTable = testReader.GetSchemaTable(); ; + } + } + + /// + /// Test that + /// throws a for missing UDT schema name. + /// + /// + /// Uses to test the illegal schema combination. + /// + /// + [Test] + [ExpectedException(typeof(ArgumentException))] + public void AddSchemaTableRowUdtMissingSchemaNameTest() + { + using (BulkDataReaderSchemaTest testReader = new BulkDataReaderSchemaTest()) + { + testReader.AllowDBNull = false; + testReader.ColumnName = "Name"; + testReader.ColumnSize = null; + testReader.IsKey = false; + testReader.IsUnique = false; + testReader.NumericPrecision = null; + testReader.NumericScale = null; + testReader.ProviderType = SqlDbType.Udt; + testReader.UdtSchema = null; + testReader.UdtType = "Type"; + testReader.XmlSchemaCollectionDatabase = null; + testReader.XmlSchemaCollectionOwningSchema = null; + testReader.XmlSchemaCollectionName = null; + + DataTable schemaTable = testReader.GetSchemaTable(); ; + } + } + + /// + /// Test that + /// throws a for empty UDT schema name. + /// + /// + /// Uses to test the illegal schema combination. + /// + /// + [Test] + [ExpectedException(typeof(ArgumentException))] + public void AddSchemaTableRowUdtEmptySchemaNameTest() + { + using (BulkDataReaderSchemaTest testReader = new BulkDataReaderSchemaTest()) + { + testReader.AllowDBNull = false; + testReader.ColumnName = "Name"; + testReader.ColumnSize = null; + testReader.IsKey = false; + testReader.IsUnique = false; + testReader.NumericPrecision = null; + testReader.NumericScale = null; + testReader.ProviderType = SqlDbType.Udt; + testReader.UdtSchema = string.Empty; + testReader.UdtType = "Type"; + testReader.XmlSchemaCollectionDatabase = null; + testReader.XmlSchemaCollectionOwningSchema = null; + testReader.XmlSchemaCollectionName = null; + + DataTable schemaTable = testReader.GetSchemaTable(); ; + } + } + + /// + /// Test that + /// throws a for missing UDT name. + /// + /// + /// Uses to test the illegal schema combination. + /// + /// + [Test] + [ExpectedException(typeof(ArgumentException))] + public void AddSchemaTableRowUdtMissingNameTest() + { + using (BulkDataReaderSchemaTest testReader = new BulkDataReaderSchemaTest()) + { + testReader.AllowDBNull = false; + testReader.ColumnName = "Name"; + testReader.ColumnSize = null; + testReader.IsKey = false; + testReader.IsUnique = false; + testReader.NumericPrecision = null; + testReader.NumericScale = null; + testReader.ProviderType = SqlDbType.Udt; + testReader.UdtSchema = "Schema"; + testReader.UdtType = null; + testReader.XmlSchemaCollectionDatabase = null; + testReader.XmlSchemaCollectionOwningSchema = null; + testReader.XmlSchemaCollectionName = null; + + DataTable schemaTable = testReader.GetSchemaTable(); ; + } + } + + /// + /// Test that + /// throws a for empty UDT name. + /// + /// + /// Uses to test the illegal schema combination. + /// + /// + [Test] + [ExpectedException(typeof(ArgumentException))] + public void AddSchemaTableRowUdtEmptyNameTest() + { + using (BulkDataReaderSchemaTest testReader = new BulkDataReaderSchemaTest()) + { + testReader.AllowDBNull = false; + testReader.ColumnName = "Name"; + testReader.ColumnSize = null; + testReader.IsKey = false; + testReader.IsUnique = false; + testReader.NumericPrecision = null; + testReader.NumericScale = null; + testReader.ProviderType = SqlDbType.Udt; + testReader.UdtSchema = "Schema"; + testReader.UdtType = string.Empty; + testReader.XmlSchemaCollectionDatabase = null; + testReader.XmlSchemaCollectionOwningSchema = null; + testReader.XmlSchemaCollectionName = null; + + DataTable schemaTable = testReader.GetSchemaTable(); ; + } + } + + /// + /// Test that + /// throws a for varbinary column with a column size that is too large (>8000). + /// + /// + /// Uses to test the illegal schema combination. + /// + /// + [Test] + [ExpectedException(typeof(ArgumentOutOfRangeException))] + public void AddSchemaTableRowVarBinaryWithTooLargeSizeTest() + { + using (BulkDataReaderSchemaTest testReader = new BulkDataReaderSchemaTest()) + { + testReader.AllowDBNull = false; + testReader.ColumnName = "Name"; + testReader.ColumnSize = 8001; + testReader.IsKey = false; + testReader.IsUnique = false; + testReader.NumericPrecision = null; + testReader.NumericScale = null; + testReader.ProviderType = SqlDbType.VarBinary; + testReader.UdtSchema = null; + testReader.UdtType = null; + testReader.XmlSchemaCollectionDatabase = null; + testReader.XmlSchemaCollectionOwningSchema = null; + testReader.XmlSchemaCollectionName = null; + + DataTable schemaTable = testReader.GetSchemaTable(); ; + } + } + + /// + /// Test that + /// throws a for varchar column with a column size that is too large (>8000). + /// + /// + /// Uses to test the illegal schema combination. + /// + /// + [Test] + [ExpectedException(typeof(ArgumentOutOfRangeException))] + public void AddSchemaTableRowVarCharWithTooLargeSizeTest() + { + using (BulkDataReaderSchemaTest testReader = new BulkDataReaderSchemaTest()) + { + testReader.AllowDBNull = false; + testReader.ColumnName = "Name"; + testReader.ColumnSize = 8001; + testReader.IsKey = false; + testReader.IsUnique = false; + testReader.NumericPrecision = null; + testReader.NumericScale = null; + testReader.ProviderType = SqlDbType.VarChar; + testReader.UdtSchema = null; + testReader.UdtType = null; + testReader.XmlSchemaCollectionDatabase = null; + testReader.XmlSchemaCollectionOwningSchema = null; + testReader.XmlSchemaCollectionName = null; + + DataTable schemaTable = testReader.GetSchemaTable(); ; + } + } + + /// + /// Test that + /// throws a for null xml collection name but with a name for the database. + /// + /// + /// Uses to test the illegal schema combination. + /// + /// + [Test] + [ExpectedException(typeof(ArgumentException))] + public void AddSchemaTableRowXmlNullNameWithDatabaseNameTest() + { + using (BulkDataReaderSchemaTest testReader = new BulkDataReaderSchemaTest()) + { + testReader.AllowDBNull = false; + testReader.ColumnName = "Name"; + testReader.ColumnSize = null; + testReader.IsKey = false; + testReader.IsUnique = false; + testReader.NumericPrecision = null; + testReader.NumericScale = null; + testReader.ProviderType = SqlDbType.Xml; + testReader.UdtSchema = null; + testReader.UdtType = null; + testReader.XmlSchemaCollectionDatabase = "Database"; + testReader.XmlSchemaCollectionOwningSchema = null; + testReader.XmlSchemaCollectionName = null; + + DataTable schemaTable = testReader.GetSchemaTable(); ; + } + } + + /// + /// Test that + /// throws a for null xml collection name. + /// + /// + /// Uses to test the illegal schema combination. + /// + /// + [Test] + [ExpectedException(typeof(ArgumentException))] + public void AddSchemaTableRowXmlNullNameWithOwningSchemaNameTest() + { + using (BulkDataReaderSchemaTest testReader = new BulkDataReaderSchemaTest()) + { + testReader.AllowDBNull = false; + testReader.ColumnName = "Name"; + testReader.ColumnSize = null; + testReader.IsKey = false; + testReader.IsUnique = false; + testReader.NumericPrecision = null; + testReader.NumericScale = null; + testReader.ProviderType = SqlDbType.Xml; + testReader.UdtSchema = null; + testReader.UdtType = null; + testReader.XmlSchemaCollectionDatabase = null; + testReader.XmlSchemaCollectionOwningSchema = "Schema"; + testReader.XmlSchemaCollectionName = null; + + DataTable schemaTable = testReader.GetSchemaTable(); ; + } + } + + /// + /// Test that + /// throws a for empty xml collection database name. + /// + /// + /// Uses to test the illegal schema combination. + /// + /// + [Test] + [ExpectedException(typeof(ArgumentException))] + public void AddSchemaTableRowXmlEmptyDatabaseNameTest() + { + using (BulkDataReaderSchemaTest testReader = new BulkDataReaderSchemaTest()) + { + testReader.AllowDBNull = false; + testReader.ColumnName = "Name"; + testReader.ColumnSize = null; + testReader.IsKey = false; + testReader.IsUnique = false; + testReader.NumericPrecision = null; + testReader.NumericScale = null; + testReader.ProviderType = SqlDbType.Xml; + testReader.UdtSchema = null; + testReader.UdtType = null; + testReader.XmlSchemaCollectionDatabase = string.Empty; + testReader.XmlSchemaCollectionOwningSchema = "Schema"; + testReader.XmlSchemaCollectionName = "Xml"; + + DataTable schemaTable = testReader.GetSchemaTable(); ; + } + } + + /// + /// Test that + /// throws a for empty xml collection owning schema name. + /// + /// + /// Uses to test the illegal schema combination. + /// + /// + [Test] + [ExpectedException(typeof(ArgumentException))] + public void AddSchemaTableRowXmlEmptyOwningSchemaNameTest() + { + using (BulkDataReaderSchemaTest testReader = new BulkDataReaderSchemaTest()) + { + testReader.AllowDBNull = false; + testReader.ColumnName = "Name"; + testReader.ColumnSize = null; + testReader.IsKey = false; + testReader.IsUnique = false; + testReader.NumericPrecision = null; + testReader.NumericScale = null; + testReader.ProviderType = SqlDbType.Xml; + testReader.UdtSchema = null; + testReader.UdtType = null; + testReader.XmlSchemaCollectionDatabase = "Database"; + testReader.XmlSchemaCollectionOwningSchema = string.Empty; + testReader.XmlSchemaCollectionName = "Xml"; + + DataTable schemaTable = testReader.GetSchemaTable(); ; + } + } + + /// + /// Test that + /// throws a for empty xml collection name. + /// + /// + /// Uses to test the illegal schema combination. + /// + /// + [Test] + [ExpectedException(typeof(ArgumentException))] + public void AddSchemaTableRowXmlEmptyNameTest() + { + using (BulkDataReaderSchemaTest testReader = new BulkDataReaderSchemaTest()) + { + testReader.AllowDBNull = false; + testReader.ColumnName = "Name"; + testReader.ColumnSize = null; + testReader.IsKey = false; + testReader.IsUnique = false; + testReader.NumericPrecision = null; + testReader.NumericScale = null; + testReader.ProviderType = SqlDbType.Xml; + testReader.UdtSchema = null; + testReader.UdtType = null; + testReader.XmlSchemaCollectionDatabase = "Database"; + testReader.XmlSchemaCollectionOwningSchema = "Schema"; + testReader.XmlSchemaCollectionName = string.Empty; + + DataTable schemaTable = testReader.GetSchemaTable(); ; + } + } + + /// + /// Test that + /// throws a for a structured column (which is illegal). + /// + /// + /// Uses to test the illegal schema combination. + /// + /// + [Test] + [ExpectedException(typeof(ArgumentOutOfRangeException))] + public void AddSchemaTableRowStructuredTypeTest() + { + using (BulkDataReaderSchemaTest testReader = new BulkDataReaderSchemaTest()) + { + testReader.AllowDBNull = false; + testReader.ColumnName = "Name"; + testReader.ColumnSize = null; + testReader.IsKey = false; + testReader.IsUnique = false; + testReader.NumericPrecision = null; + testReader.NumericScale = null; + testReader.ProviderType = SqlDbType.Structured; + testReader.UdtSchema = null; + testReader.UdtType = null; + testReader.XmlSchemaCollectionDatabase = null; + testReader.XmlSchemaCollectionOwningSchema = null; + testReader.XmlSchemaCollectionName = null; + + DataTable schemaTable = testReader.GetSchemaTable(); ; + } + } + + /// + /// Test that + /// throws a for a timestamp column (which is illegal). + /// + /// + /// Uses to test the illegal schema combination. + /// + /// + [Test] + [ExpectedException(typeof(ArgumentOutOfRangeException))] + public void AddSchemaTableRowTimestampTypeTest() + { + using (BulkDataReaderSchemaTest testReader = new BulkDataReaderSchemaTest()) + { + testReader.AllowDBNull = false; + testReader.ColumnName = "Name"; + testReader.ColumnSize = null; + testReader.IsKey = false; + testReader.IsUnique = false; + testReader.NumericPrecision = null; + testReader.NumericScale = null; + testReader.ProviderType = SqlDbType.Timestamp; + testReader.UdtSchema = null; + testReader.UdtType = null; + testReader.XmlSchemaCollectionDatabase = null; + testReader.XmlSchemaCollectionOwningSchema = null; + testReader.XmlSchemaCollectionName = null; + + DataTable schemaTable = testReader.GetSchemaTable(); ; + } + } + + /// + /// Test that + /// throws an for a column with an unallowed optional column set. + /// + /// + /// Uses to test the illegal schema combination. + /// + /// + [Test] + public void AddSchemaTableRowUnallowedOptionalColumnTest() + { + + // Column size set + using (BulkDataReaderSchemaTest testReader = new BulkDataReaderSchemaTest()) + { + testReader.AllowDBNull = false; + testReader.ColumnName = "Name"; + testReader.ColumnSize = 5; + testReader.IsKey = false; + testReader.IsUnique = false; + testReader.NumericPrecision = null; + testReader.NumericScale = null; + testReader.UdtSchema = null; + testReader.UdtType = null; + testReader.XmlSchemaCollectionDatabase = null; + testReader.XmlSchemaCollectionOwningSchema = null; + testReader.XmlSchemaCollectionName = null; + + foreach (SqlDbType dbtype in new List { SqlDbType.BigInt, SqlDbType.Bit, SqlDbType.Date, SqlDbType.DateTime, SqlDbType.DateTime2, + SqlDbType.DateTimeOffset, SqlDbType.Image, SqlDbType.Int, SqlDbType.Money, SqlDbType.Real, + SqlDbType.SmallDateTime, SqlDbType.SmallInt, SqlDbType.SmallMoney, SqlDbType.Structured, SqlDbType.Text, + SqlDbType.Time, SqlDbType.Timestamp, SqlDbType.TinyInt, SqlDbType.Udt, SqlDbType.UniqueIdentifier, + SqlDbType.Variant, SqlDbType.Xml }) + { + testReader.ProviderType = dbtype; + + try + { + DataTable schemaTable = testReader.GetSchemaTable(); + + Assert.Fail(); + } + catch (ArgumentException) + { + } + } + } + + // Numeric precision set + using (BulkDataReaderSchemaTest testReader = new BulkDataReaderSchemaTest()) + { + testReader.AllowDBNull = false; + testReader.ColumnName = "Name"; + testReader.ColumnSize = null; + testReader.IsKey = false; + testReader.IsUnique = false; + testReader.NumericPrecision = 5; + testReader.NumericScale = null; + testReader.UdtSchema = null; + testReader.UdtType = null; + testReader.XmlSchemaCollectionDatabase = null; + testReader.XmlSchemaCollectionOwningSchema = null; + testReader.XmlSchemaCollectionName = null; + + foreach (SqlDbType dbtype in new List { SqlDbType.BigInt, SqlDbType.Binary, SqlDbType.Bit, SqlDbType.Char, SqlDbType.Date, + SqlDbType.DateTime, SqlDbType.Image, SqlDbType.Int, SqlDbType.Money, SqlDbType.NChar, + SqlDbType.NText, SqlDbType.NVarChar, SqlDbType.Real, SqlDbType.SmallDateTime, SqlDbType.SmallInt, + SqlDbType.SmallMoney, SqlDbType.Structured, SqlDbType.Text, SqlDbType.Timestamp, SqlDbType.TinyInt, + SqlDbType.Udt, SqlDbType.UniqueIdentifier, SqlDbType.VarBinary, SqlDbType.VarChar, SqlDbType.Variant, + SqlDbType.Xml }) + { + testReader.ProviderType = dbtype; + + try + { + DataTable schemaTable = testReader.GetSchemaTable(); + + Assert.Fail(); + } + catch (ArgumentException) + { + } + } + } + + // Numeric scale set + using (BulkDataReaderSchemaTest testReader = new BulkDataReaderSchemaTest()) + { + testReader.AllowDBNull = false; + testReader.ColumnName = "Name"; + testReader.ColumnSize = null; + testReader.IsKey = false; + testReader.IsUnique = false; + testReader.NumericPrecision = 5; + testReader.NumericScale = 3; + testReader.UdtSchema = null; + testReader.UdtType = null; + testReader.XmlSchemaCollectionDatabase = null; + testReader.XmlSchemaCollectionOwningSchema = null; + testReader.XmlSchemaCollectionName = null; + + foreach (SqlDbType dbtype in new List { SqlDbType.BigInt, SqlDbType.Binary, SqlDbType.Bit, SqlDbType.Char, SqlDbType.Date, + SqlDbType.DateTime, SqlDbType.DateTime2, SqlDbType.DateTimeOffset, SqlDbType.Image, SqlDbType.Int, + SqlDbType.Money, SqlDbType.NChar, SqlDbType.NText, SqlDbType.NVarChar, SqlDbType.Real, + SqlDbType.SmallDateTime, SqlDbType.SmallInt, SqlDbType.SmallMoney, SqlDbType.Structured, SqlDbType.Text, + SqlDbType.Time, SqlDbType.Timestamp, SqlDbType.TinyInt, SqlDbType.Udt, SqlDbType.UniqueIdentifier, + SqlDbType.VarBinary, SqlDbType.VarChar, SqlDbType.Variant, SqlDbType.Xml }) + { + testReader.ProviderType = dbtype; + + try + { + DataTable schemaTable = testReader.GetSchemaTable(); + + Assert.Fail(); + } + catch (ArgumentException) + { + } + } + } + + // Numeric scale set + using (BulkDataReaderSchemaTest testReader = new BulkDataReaderSchemaTest()) + { + testReader.AllowDBNull = false; + testReader.ColumnName = "Name"; + testReader.ColumnSize = null; + testReader.IsKey = false; + testReader.IsUnique = false; + testReader.NumericPrecision = 5; + testReader.NumericScale = 3; + testReader.UdtSchema = null; + testReader.UdtType = null; + testReader.XmlSchemaCollectionDatabase = null; + testReader.XmlSchemaCollectionOwningSchema = null; + testReader.XmlSchemaCollectionName = null; + + foreach (SqlDbType dbtype in new List { SqlDbType.BigInt, SqlDbType.Binary, SqlDbType.Bit, SqlDbType.Char, SqlDbType.Date, + SqlDbType.DateTime, SqlDbType.DateTime2, SqlDbType.DateTimeOffset, SqlDbType.Image, SqlDbType.Int, + SqlDbType.Money, SqlDbType.NChar, SqlDbType.NText, SqlDbType.NVarChar, SqlDbType.Real, + SqlDbType.SmallDateTime, SqlDbType.SmallInt, SqlDbType.SmallMoney, SqlDbType.Structured, SqlDbType.Text, + SqlDbType.Time, SqlDbType.Timestamp, SqlDbType.TinyInt, SqlDbType.Udt, SqlDbType.UniqueIdentifier, + SqlDbType.VarBinary, SqlDbType.VarChar, SqlDbType.Variant, SqlDbType.Xml }) + { + testReader.ProviderType = dbtype; + + try + { + DataTable schemaTable = testReader.GetSchemaTable(); + + Assert.Fail(); + } + catch (ArgumentException) + { + } + } + } + + // UDT type name set + using (BulkDataReaderSchemaTest testReader = new BulkDataReaderSchemaTest()) + { + testReader.AllowDBNull = false; + testReader.ColumnName = "Name"; + testReader.ColumnSize = null; + testReader.IsKey = false; + testReader.IsUnique = false; + testReader.NumericPrecision = null; + testReader.NumericScale = null; + testReader.UdtSchema = null; + testReader.UdtType = "Type"; + testReader.XmlSchemaCollectionDatabase = null; + testReader.XmlSchemaCollectionOwningSchema = null; + testReader.XmlSchemaCollectionName = null; + + foreach (SqlDbType dbtype in new List { SqlDbType.BigInt, SqlDbType.Binary, SqlDbType.Bit, SqlDbType.Char, SqlDbType.Date, + SqlDbType.DateTime, SqlDbType.DateTime2, SqlDbType.DateTimeOffset, SqlDbType.Decimal, SqlDbType.Float, + SqlDbType.Image, SqlDbType.Int, SqlDbType.Money, SqlDbType.NChar, SqlDbType.NText, + SqlDbType.NVarChar, SqlDbType.Real, SqlDbType.SmallDateTime, SqlDbType.SmallInt, SqlDbType.SmallMoney, + SqlDbType.Structured, SqlDbType.Text, SqlDbType.Time, SqlDbType.Timestamp, SqlDbType.TinyInt, + SqlDbType.UniqueIdentifier, SqlDbType.VarBinary, SqlDbType.VarChar, SqlDbType.Variant, SqlDbType.Xml }) + { + testReader.ProviderType = dbtype; + + try + { + DataTable schemaTable = testReader.GetSchemaTable(); + + Assert.Fail(); + } + catch (ArgumentException) + { + } + } + } + + // UDT schema and type name set + using (BulkDataReaderSchemaTest testReader = new BulkDataReaderSchemaTest()) + { + testReader.AllowDBNull = false; + testReader.ColumnName = "Name"; + testReader.ColumnSize = null; + testReader.IsKey = false; + testReader.IsUnique = false; + testReader.NumericPrecision = null; + testReader.NumericScale = null; + testReader.UdtSchema = "Schema"; + testReader.UdtType = "Type"; + testReader.XmlSchemaCollectionDatabase = null; + testReader.XmlSchemaCollectionOwningSchema = null; + testReader.XmlSchemaCollectionName = null; + + foreach (SqlDbType dbtype in new List { SqlDbType.BigInt, SqlDbType.Binary, SqlDbType.Bit, SqlDbType.Char, SqlDbType.Date, + SqlDbType.DateTime, SqlDbType.DateTime2, SqlDbType.DateTimeOffset, SqlDbType.Decimal, SqlDbType.Float, + SqlDbType.Image, SqlDbType.Int, SqlDbType.Money, SqlDbType.NChar, SqlDbType.NText, + SqlDbType.NVarChar, SqlDbType.Real, SqlDbType.SmallDateTime, SqlDbType.SmallInt, SqlDbType.SmallMoney, + SqlDbType.Structured, SqlDbType.Text, SqlDbType.Time, SqlDbType.Timestamp, SqlDbType.TinyInt, + SqlDbType.UniqueIdentifier, SqlDbType.VarBinary, SqlDbType.VarChar, SqlDbType.Variant, SqlDbType.Xml }) + { + testReader.ProviderType = dbtype; + + try + { + DataTable schemaTable = testReader.GetSchemaTable(); + + Assert.Fail(); + } + catch (ArgumentException) + { + } + } + } + + // XML type name set + using (BulkDataReaderSchemaTest testReader = new BulkDataReaderSchemaTest()) + { + testReader.AllowDBNull = false; + testReader.ColumnName = "Name"; + testReader.ColumnSize = null; + testReader.IsKey = false; + testReader.IsUnique = false; + testReader.NumericPrecision = null; + testReader.NumericScale = null; + testReader.UdtSchema = null; + testReader.UdtType = null; + testReader.XmlSchemaCollectionDatabase = null; + testReader.XmlSchemaCollectionOwningSchema = null; + testReader.XmlSchemaCollectionName = "Name"; + + foreach (SqlDbType dbtype in new List { SqlDbType.BigInt, SqlDbType.Binary, SqlDbType.Bit, SqlDbType.Char, SqlDbType.Date, + SqlDbType.DateTime, SqlDbType.DateTime2, SqlDbType.DateTimeOffset, SqlDbType.Decimal, SqlDbType.Float, + SqlDbType.Image, SqlDbType.Int, SqlDbType.Money, SqlDbType.NChar, SqlDbType.NText, + SqlDbType.NVarChar, SqlDbType.Real, SqlDbType.SmallDateTime, SqlDbType.SmallInt, SqlDbType.SmallMoney, + SqlDbType.Structured, SqlDbType.Text, SqlDbType.Time, SqlDbType.Timestamp, SqlDbType.TinyInt, + SqlDbType.UniqueIdentifier, SqlDbType.VarBinary, SqlDbType.VarChar, SqlDbType.Variant, SqlDbType.Udt }) + { + testReader.ProviderType = dbtype; + + try + { + DataTable schemaTable = testReader.GetSchemaTable(); + + Assert.Fail(); + } + catch (ArgumentException) + { + } + } + } + + // XML owning schema and type name set + using (BulkDataReaderSchemaTest testReader = new BulkDataReaderSchemaTest()) + { + testReader.AllowDBNull = false; + testReader.ColumnName = "Name"; + testReader.ColumnSize = null; + testReader.IsKey = false; + testReader.IsUnique = false; + testReader.NumericPrecision = null; + testReader.NumericScale = null; + testReader.UdtSchema = null; + testReader.UdtType = null; + testReader.XmlSchemaCollectionDatabase = null; + testReader.XmlSchemaCollectionOwningSchema = "Schema"; + testReader.XmlSchemaCollectionName = "Name"; + + foreach (SqlDbType dbtype in new List { SqlDbType.BigInt, SqlDbType.Binary, SqlDbType.Bit, SqlDbType.Char, SqlDbType.Date, + SqlDbType.DateTime, SqlDbType.DateTime2, SqlDbType.DateTimeOffset, SqlDbType.Decimal, SqlDbType.Float, + SqlDbType.Image, SqlDbType.Int, SqlDbType.Money, SqlDbType.NChar, SqlDbType.NText, + SqlDbType.NVarChar, SqlDbType.Real, SqlDbType.SmallDateTime, SqlDbType.SmallInt, SqlDbType.SmallMoney, + SqlDbType.Structured, SqlDbType.Text, SqlDbType.Time, SqlDbType.Timestamp, SqlDbType.TinyInt, + SqlDbType.UniqueIdentifier, SqlDbType.VarBinary, SqlDbType.VarChar, SqlDbType.Variant, SqlDbType.Udt }) + { + testReader.ProviderType = dbtype; + + try + { + DataTable schemaTable = testReader.GetSchemaTable(); + + Assert.Fail(); + } + catch (ArgumentException) + { + } + } + } + + // XML database, owning schema and type name set + using (BulkDataReaderSchemaTest testReader = new BulkDataReaderSchemaTest()) + { + testReader.AllowDBNull = false; + testReader.ColumnName = "Name"; + testReader.ColumnSize = null; + testReader.IsKey = false; + testReader.IsUnique = false; + testReader.NumericPrecision = null; + testReader.NumericScale = null; + testReader.UdtSchema = null; + testReader.UdtType = null; + testReader.XmlSchemaCollectionDatabase = "Database"; + testReader.XmlSchemaCollectionOwningSchema = "Schema"; + testReader.XmlSchemaCollectionName = "Name"; + + foreach (SqlDbType dbtype in new List { SqlDbType.BigInt, SqlDbType.Binary, SqlDbType.Bit, SqlDbType.Char, SqlDbType.Date, + SqlDbType.DateTime, SqlDbType.DateTime2, SqlDbType.DateTimeOffset, SqlDbType.Decimal, SqlDbType.Float, + SqlDbType.Image, SqlDbType.Int, SqlDbType.Money, SqlDbType.NChar, SqlDbType.NText, + SqlDbType.NVarChar, SqlDbType.Real, SqlDbType.SmallDateTime, SqlDbType.SmallInt, SqlDbType.SmallMoney, + SqlDbType.Structured, SqlDbType.Text, SqlDbType.Time, SqlDbType.Timestamp, SqlDbType.TinyInt, + SqlDbType.UniqueIdentifier, SqlDbType.VarBinary, SqlDbType.VarChar, SqlDbType.Variant, SqlDbType.Udt }) + { + testReader.ProviderType = dbtype; + + try + { + DataTable schemaTable = testReader.GetSchemaTable(); + + Assert.Fail(); + } + catch (ArgumentException) + { + } + } + } + } + + #endregion; + + #region Rowset tests + + /// + /// Test that is functioning correctly. + /// + /// + [Test] + public void CloseTest() + { + BulkDataReaderSubclass testReader = new BulkDataReaderSubclass(); + + testReader.Close(); + + Assert.IsTrue(testReader.IsClosed); + } + + /// + /// Test that is functioning correctly. + /// + /// + /// Because nested row sets are not supported, this should always return 0; + /// + /// + [Test] + public void DepthTest() + { + using (BulkDataReaderSubclass testReader = new BulkDataReaderSubclass()) + { + Assert.IsTrue(testReader.Read()); + + Assert.AreEqual(testReader.Depth, 0); + } + } + + /// + /// Test that is functioning correctly. + /// + /// + /// Because nested row sets are not supported, this should always return null; + /// + /// + [Test] + public void GetDataTest() + { + using (BulkDataReaderSubclass testReader = new BulkDataReaderSubclass()) + { + Assert.IsTrue(testReader.Read()); + + Assert.IsTrue(testReader.FieldCount > 0); + + Assert.IsNull(testReader.GetData(0)); + } + } + + /// + /// Test and related functions. + /// + /// + /// Uses to test legal schema combinations. + /// + [Test] + public void GetValueTest() + { + using (BulkDataReaderSubclass testReader = new BulkDataReaderSubclass()) + { + Assert.IsTrue(testReader.Read()); + + // this[int] + for (int column = 0; column < BulkDataReaderSubclass.ExpectedResultSet.Count; column++) + { + Assert.AreEqual(testReader[column], BulkDataReaderSubclass.ExpectedResultSet[column]); + } + + // this[string] + for (int column = 0; column < BulkDataReaderSubclass.ExpectedResultSet.Count; column++) + { + Assert.AreEqual(testReader[testReader.GetName(column)], BulkDataReaderSubclass.ExpectedResultSet[column]); + + Assert.AreEqual(testReader[testReader.GetName(column).ToUpperInvariant()], BulkDataReaderSubclass.ExpectedResultSet[column]); + } + + // GetValues + { + object[] values = new object[BulkDataReaderSubclass.ExpectedResultSet.Count]; + object[] expectedValues = new object[BulkDataReaderSubclass.ExpectedResultSet.Count]; + + Assert.AreEqual(testReader.GetValues(values), values.Length); + + BulkDataReaderSubclass.ExpectedResultSet.CopyTo(expectedValues, 0); + + Assert.IsTrue(BulkDataReaderTest.ArraysMatch(values, expectedValues)); + } + + // Typed getters + { + int currentColumn = 0; + + Assert.AreEqual(testReader.GetInt64(currentColumn), BulkDataReaderSubclass.ExpectedResultSet[currentColumn]); + currentColumn++; + + { + byte[] expectedResult = (byte[])BulkDataReaderSubclass.ExpectedResultSet[currentColumn]; + int expectedLength = expectedResult.Length; + byte[] buffer = new byte[expectedLength]; + + Assert.AreEqual(testReader.GetBytes(currentColumn, 0, buffer, 0, expectedLength), expectedLength); + + Assert.IsTrue(BulkDataReaderTest.ArraysMatch(buffer, expectedResult)); + } + currentColumn++; + + Assert.AreEqual(testReader.GetBoolean(currentColumn), BulkDataReaderSubclass.ExpectedResultSet[currentColumn]); + currentColumn++; + + Assert.AreEqual(testReader.IsDBNull(currentColumn), BulkDataReaderSubclass.ExpectedResultSet[currentColumn] == null); + currentColumn++; + + Assert.AreEqual(testReader.GetChar(currentColumn), BulkDataReaderSubclass.ExpectedResultSet[currentColumn]); + currentColumn++; + + Assert.AreEqual(testReader.GetChar(currentColumn), ((char[])BulkDataReaderSubclass.ExpectedResultSet[currentColumn])[0]); + currentColumn++; + + Assert.AreEqual(testReader.GetChar(currentColumn), ((string)BulkDataReaderSubclass.ExpectedResultSet[currentColumn])[0]); + currentColumn++; + + Assert.AreEqual(testReader.GetString(currentColumn), BulkDataReaderSubclass.ExpectedResultSet[currentColumn]); + + { + char[] expectedResult = ((string)BulkDataReaderSubclass.ExpectedResultSet[currentColumn]).ToCharArray(); + int expectedLength = expectedResult.Length; + char[] buffer = new char[expectedLength]; + + Assert.AreEqual(testReader.GetChars(currentColumn, 0, buffer, 0, expectedLength), expectedLength); + + Assert.IsTrue(BulkDataReaderTest.ArraysMatch(buffer, expectedResult)); + } + + currentColumn++; + + Assert.AreEqual(testReader.GetDateTime(currentColumn), BulkDataReaderSubclass.ExpectedResultSet[currentColumn]); + currentColumn++; + + Assert.AreEqual(testReader.GetDateTime(currentColumn), BulkDataReaderSubclass.ExpectedResultSet[currentColumn]); + currentColumn++; + + Assert.AreEqual(testReader.GetDateTime(currentColumn), BulkDataReaderSubclass.ExpectedResultSet[currentColumn]); + currentColumn++; + + Assert.AreEqual(testReader.GetDateTime(currentColumn), BulkDataReaderSubclass.ExpectedResultSet[currentColumn]); + currentColumn++; + + Assert.AreEqual(testReader.GetDateTimeOffset(currentColumn), BulkDataReaderSubclass.ExpectedResultSet[currentColumn]); + currentColumn++; + + Assert.AreEqual(testReader.GetDateTimeOffset(currentColumn), BulkDataReaderSubclass.ExpectedResultSet[currentColumn]); + currentColumn++; + + Assert.AreEqual(testReader.GetDecimal(currentColumn), BulkDataReaderSubclass.ExpectedResultSet[currentColumn]); + currentColumn++; + + Assert.AreEqual(testReader.GetDouble(currentColumn), BulkDataReaderSubclass.ExpectedResultSet[currentColumn]); + currentColumn++; + + { + byte[] expectedResult = (byte[])BulkDataReaderSubclass.ExpectedResultSet[currentColumn]; + int expectedLength = expectedResult.Length; + byte[] buffer = new byte[expectedLength]; + + Assert.AreEqual(testReader.GetBytes(currentColumn, 0, buffer, 0, expectedLength), expectedLength); + + Assert.IsTrue(BulkDataReaderTest.ArraysMatch(buffer, expectedResult)); + } + currentColumn++; + + Assert.AreEqual(testReader.GetInt32(currentColumn), BulkDataReaderSubclass.ExpectedResultSet[currentColumn]); + currentColumn++; + + Assert.AreEqual(testReader.GetDecimal(currentColumn), BulkDataReaderSubclass.ExpectedResultSet[currentColumn]); + currentColumn++; + + Assert.AreEqual(testReader.GetString(currentColumn), BulkDataReaderSubclass.ExpectedResultSet[currentColumn]); + currentColumn++; + + Assert.AreEqual(testReader.GetString(currentColumn), BulkDataReaderSubclass.ExpectedResultSet[currentColumn]); + currentColumn++; + + Assert.AreEqual(testReader.GetString(currentColumn), BulkDataReaderSubclass.ExpectedResultSet[currentColumn]); + currentColumn++; + + Assert.AreEqual(testReader.GetString(currentColumn), BulkDataReaderSubclass.ExpectedResultSet[currentColumn]); + currentColumn++; + + Assert.AreEqual(testReader.GetFloat(currentColumn), BulkDataReaderSubclass.ExpectedResultSet[currentColumn]); + currentColumn++; + + Assert.AreEqual(testReader.GetDateTime(currentColumn), BulkDataReaderSubclass.ExpectedResultSet[currentColumn]); + currentColumn++; + + Assert.AreEqual(testReader.GetInt16(currentColumn), BulkDataReaderSubclass.ExpectedResultSet[currentColumn]); + currentColumn++; + + Assert.AreEqual(testReader.GetDecimal(currentColumn), BulkDataReaderSubclass.ExpectedResultSet[currentColumn]); + currentColumn++; + + Assert.AreEqual(testReader.GetString(currentColumn), BulkDataReaderSubclass.ExpectedResultSet[currentColumn]); + currentColumn++; + + Assert.AreEqual(testReader.GetTimeSpan(currentColumn), BulkDataReaderSubclass.ExpectedResultSet[currentColumn]); + currentColumn++; + + Assert.AreEqual(testReader.GetTimeSpan(currentColumn), BulkDataReaderSubclass.ExpectedResultSet[currentColumn]); + currentColumn++; + + Assert.AreEqual(testReader.GetByte(currentColumn), BulkDataReaderSubclass.ExpectedResultSet[currentColumn]); + currentColumn++; + + Assert.AreEqual(testReader.GetValue(currentColumn), BulkDataReaderSubclass.ExpectedResultSet[currentColumn]); + currentColumn++; + + Assert.AreEqual(testReader.GetGuid(currentColumn), BulkDataReaderSubclass.ExpectedResultSet[currentColumn]); + currentColumn++; + + { + byte[] expectedResult = (byte[])BulkDataReaderSubclass.ExpectedResultSet[currentColumn]; + int expectedLength = expectedResult.Length; + byte[] buffer = new byte[expectedLength]; + + Assert.AreEqual(testReader.GetBytes(currentColumn, 0, buffer, 0, expectedLength), expectedLength); + + Assert.IsTrue(BulkDataReaderTest.ArraysMatch(buffer, expectedResult)); + } + currentColumn++; + + { + byte[] expectedResult = (byte[])BulkDataReaderSubclass.ExpectedResultSet[currentColumn]; + int expectedLength = expectedResult.Length; + byte[] buffer = new byte[expectedLength]; + + Assert.AreEqual(testReader.GetBytes(currentColumn, 0, buffer, 0, expectedLength), expectedLength); + + Assert.IsTrue(BulkDataReaderTest.ArraysMatch(buffer, expectedResult)); + } + currentColumn++; + + Assert.AreEqual(testReader.GetString(currentColumn), BulkDataReaderSubclass.ExpectedResultSet[currentColumn]); + currentColumn++; + + Assert.AreEqual(testReader.GetString(currentColumn), BulkDataReaderSubclass.ExpectedResultSet[currentColumn]); + currentColumn++; + + Assert.AreEqual(testReader.GetValue(currentColumn), BulkDataReaderSubclass.ExpectedResultSet[currentColumn]); + currentColumn++; + + Assert.AreEqual(testReader.GetString(currentColumn), BulkDataReaderSubclass.ExpectedResultSet[currentColumn]); + currentColumn++; + + Assert.AreEqual(testReader.GetString(currentColumn), BulkDataReaderSubclass.ExpectedResultSet[currentColumn]); + currentColumn++; + + Assert.AreEqual(testReader.GetString(currentColumn), BulkDataReaderSubclass.ExpectedResultSet[currentColumn]); + currentColumn++; + + Assert.AreEqual(testReader.GetString(currentColumn), BulkDataReaderSubclass.ExpectedResultSet[currentColumn]); + currentColumn++; + + Assert.AreEqual(testReader.GetString(currentColumn), BulkDataReaderSubclass.ExpectedResultSet[currentColumn]); + currentColumn++; + } + } + } + + /// + /// Test throws a when + /// the index is too small. + /// + /// + /// Uses to test the method. + /// + /// + [Test] + [ExpectedException(typeof(ArgumentOutOfRangeException))] + public void GetValueIndexTooSmallTest() + { + using (BulkDataReaderSubclass testReader = new BulkDataReaderSubclass()) + { + Assert.IsTrue(testReader.Read()); + + object result = testReader.GetValue(-1); + } + } + + /// + /// Test throws a when + /// the index is too large. + /// + /// + /// Uses to test the method. + /// + /// + [Test] + [ExpectedException(typeof(ArgumentOutOfRangeException))] + public void GetValueIndexTooLargeTest() + { + using (BulkDataReaderSubclass testReader = new BulkDataReaderSubclass()) + { + Assert.IsTrue(testReader.Read()); + + object result = testReader.GetValue(testReader.FieldCount); + } + } + + /// + /// Test throws a when + /// the index is too small. + /// + /// + /// Uses to test the method. + /// + /// + [Test] + [ExpectedException(typeof(ArgumentOutOfRangeException))] + public void GetDataIndexTooSmallTest() + { + using (BulkDataReaderSubclass testReader = new BulkDataReaderSubclass()) + { + Assert.IsTrue(testReader.Read()); + + object result = testReader.GetData(-1); + } + } + + /// + /// Test throws a when + /// the index is too large. + /// + /// + /// Uses to test the method. + /// + [Test] + [ExpectedException(typeof(ArgumentOutOfRangeException))] + public void GetDataIndexTooLargeTest() + { + using (BulkDataReaderSubclass testReader = new BulkDataReaderSubclass()) + { + Assert.IsTrue(testReader.Read()); + + object result = testReader.GetData(testReader.FieldCount); + } + } + + /// + /// Test that functions correctly. + /// + /// + [Test] + public void IsDBNullTest() + { + using (BulkDataReaderSubclass testReader = new BulkDataReaderSubclass()) + { + for (int currentColumn = 0; currentColumn < testReader.FieldCount; currentColumn++) + { + Assert.AreEqual(testReader.IsDBNull(currentColumn), BulkDataReaderSubclass.ExpectedResultSet[currentColumn] == null); + } + } + } + + /// + /// Test that is functioning correctly. + /// + /// + /// Because this is a single row set, this should always return false; + /// + /// + [Test] + public void NextResultTest() + { + using (BulkDataReaderSubclass testReader = new BulkDataReaderSubclass()) + { + Assert.IsFalse(testReader.NextResult()); + } + } + + /// + /// Test that is functioning correctly. + /// + /// + /// Because this row set represents a data source, this should always return -1; + /// + /// + [Test] + public void RecordsAffectedTest() + { + using (BulkDataReaderSubclass testReader = new BulkDataReaderSubclass()) + { + Assert.IsTrue(testReader.Read()); + + Assert.AreEqual(testReader.RecordsAffected, -1); + } + } + + #endregion + + #region Test IDisposable + + /// + /// Test that the interface is functioning correctly. + /// + /// + /// + [Test] + public void IDisposableTest() + { + // Test the Dispose method + { + BulkDataReaderSubclass testReader = new BulkDataReaderSubclass(); + + testReader.Dispose(); + + Assert.IsTrue(testReader.IsClosed); + } + + // Test the finalizer method + { + BulkDataReaderSubclass testReader = new BulkDataReaderSubclass(); + + testReader = null; + + GC.Collect(); + + GC.WaitForPendingFinalizers(); + } + } + + #endregion + + #region Utility + + /// + /// Do the two arrays match exactly? + /// + /// + /// The type of the array elements. + /// + /// + /// The first array. + /// + /// + /// The second array. + /// + /// + /// True if the arrays have the same length and contents. + /// + private static bool ArraysMatch(ElementType[] left, + ElementType[] right) + { + if (left == null) + { + throw new ArgumentNullException("left"); + } + else if (right == null) + { + throw new ArgumentNullException("left"); + } + + bool result = true; + + if (left.Length != right.Length) + { + result = false; + } + else + { + for (int currentIndex = 0; currentIndex < left.Length; currentIndex++) + { + result &= object.Equals(left[currentIndex], right[currentIndex]); + } + } + + return result; + } + + #endregion + + #region Test stubs + + /// + /// A subclass of used for testing its utility functions. + /// + private class BulkDataReaderSubclass : BulkDataReader + { + + #region Constructors + + /// + /// Constructor. + /// + public BulkDataReaderSubclass() + { + } + + #endregion + + #region BulkDataReader + + /// + /// See . + /// + /// + /// Returns . + /// + protected override string SchemaName + { + get { return BulkDataReaderTest.testSchemaName; } + } + + /// + /// See . + /// + /// + /// Returns . + /// + protected override string TableName + { + get { return BulkDataReaderTest.testTableName; } + } + + /// + /// See + /// + /// + /// Creates a schema row for the various values. + /// + protected override void AddSchemaTableRows() + { + AddSchemaTableRow("BigInt", null, null, null, true, false, false, SqlDbType.BigInt, null, null, null, null, null); + AddSchemaTableRow("Binary_20", 20, null, null, false, true, false, SqlDbType.Binary, null, null, null, null, null); + AddSchemaTableRow("Bit", null, null, null, false, false, true, SqlDbType.Bit, null, null, null, null, null); + AddSchemaTableRow("Bit_null", null, null, null, false, false, true, SqlDbType.Bit, null, null, null, null, null); + AddSchemaTableRow("Char_Char", 1, null, null, false, false, false, SqlDbType.Char, null, null, null, null, null); + AddSchemaTableRow("Char_Char_Array", 1, null, null, false, false, false, SqlDbType.Char, null, null, null, null, null); + AddSchemaTableRow("Char_String", 1, null, null, false, false, false, SqlDbType.Char, null, null, null, null, null); + AddSchemaTableRow("Char_20_String", 20, null, null, false, false, false, SqlDbType.Char, null, null, null, null, null); + AddSchemaTableRow("Date", null, null, null, false, false, false, SqlDbType.Date, null, null, null, null, null); + AddSchemaTableRow("DateTime", null, null, null, false, false, false, SqlDbType.DateTime, null, null, null, null, null); + AddSchemaTableRow("DateTime2", null, null, null, false, false, false, SqlDbType.DateTime2, null, null, null, null, null); + AddSchemaTableRow("DateTime2_5", null, 5, null, false, false, false, SqlDbType.DateTime2, null, null, null, null, null); + AddSchemaTableRow("DateTimeOffset", null, null, null, false, false, false, SqlDbType.DateTimeOffset, null, null, null, null, null); + AddSchemaTableRow("DateTimeOffset_5", null, 5, null, false, false, false, SqlDbType.DateTimeOffset, null, null, null, null, null); + AddSchemaTableRow("Decimal_20_10", null, 20, 10, false, false, false, SqlDbType.Decimal, null, null, null, null, null); + AddSchemaTableRow("Float_50", null, 50, null, false, false, false, SqlDbType.Float, null, null, null, null, null); + AddSchemaTableRow("Image", null, null, null, false, false, false, SqlDbType.Image, null, null, null, null, null); + AddSchemaTableRow("Int", null, null, null, false, false, false, SqlDbType.Int, null, null, null, null, null); + AddSchemaTableRow("Money", null, null, null, false, false, false, SqlDbType.Money, null, null, null, null, null); + AddSchemaTableRow("NChar_20", 20, null, null, false, false, false, SqlDbType.NChar, null, null, null, null, null); + AddSchemaTableRow("NText", null, null, null, false, false, false, SqlDbType.NText, null, null, null, null, null); + AddSchemaTableRow("NVarChar_20", 20, null, null, false, false, false, SqlDbType.NVarChar, null, null, null, null, null); + AddSchemaTableRow("NVarChar_Max", null, null, null, false, false, false, SqlDbType.NVarChar, null, null, null, null, null); + AddSchemaTableRow("Real", null, null, null, false, false, false, SqlDbType.Real, null, null, null, null, null); + AddSchemaTableRow("SmallDateTime", null, null, null, false, false, false, SqlDbType.SmallDateTime, null, null, null, null, null); + AddSchemaTableRow("SmallInt", null, null, null, false, false, false, SqlDbType.SmallInt, null, null, null, null, null); + AddSchemaTableRow("SmallMoney", null, null, null, false, false, false, SqlDbType.SmallMoney, null, null, null, null, null); + AddSchemaTableRow("Text", null, null, null, false, false, false, SqlDbType.Text, null, null, null, null, null); + AddSchemaTableRow("Time", null, null, null, false, false, false, SqlDbType.Time, null, null, null, null, null); + AddSchemaTableRow("Time_5", null, 5, null, false, false, false, SqlDbType.Time, null, null, null, null, null); + AddSchemaTableRow("TinyInt", null, null, null, false, false, false, SqlDbType.TinyInt, null, null, null, null, null); + AddSchemaTableRow("Udt", null, null, null, false, false, false, SqlDbType.Udt, BulkDataReaderTest.testUdtSchemaName, BulkDataReaderTest.testUdtName, null, null, null); + AddSchemaTableRow("UniqueIdentifier", null, null, null, false, false, false, SqlDbType.UniqueIdentifier, null, null, null, null, null); + AddSchemaTableRow("VarBinary_20", 20, null, null, false, false, false, SqlDbType.VarBinary, null, null, null, null, null); + AddSchemaTableRow("VarBinary_Max", null, null, null, false, false, false, SqlDbType.VarBinary, null, null, null, null, null); + AddSchemaTableRow("VarChar_20", 20, null, null, false, false, false, SqlDbType.VarChar, null, null, null, null, null); + AddSchemaTableRow("VarChar_Max", null, null, null, false, false, false, SqlDbType.VarChar, null, null, null, null, null); + AddSchemaTableRow("Variant", null, null, null, false, false, false, SqlDbType.Variant, null, null, null, null, null); + AddSchemaTableRow("Xml_Database", null, null, null, false, false, false, SqlDbType.Xml, null, null, BulkDataReaderTest.testXmlSchemaCollectionDatabaseName, BulkDataReaderTest.testXMLSchemaCollectionSchemaName, BulkDataReaderTest.testXMLSchemaCollectionName); + AddSchemaTableRow("Xml_Database_XML", null, null, null, false, false, false, SqlDbType.Xml, null, null, BulkDataReaderTest.testXmlSchemaCollectionDatabaseName, BulkDataReaderTest.testXMLSchemaCollectionSchemaName, BulkDataReaderTest.testXMLSchemaCollectionName); + AddSchemaTableRow("Xml_Schema", null, null, null, false, false, false, SqlDbType.Xml, null, null, null, BulkDataReaderTest.testXMLSchemaCollectionSchemaName, BulkDataReaderTest.testXMLSchemaCollectionName); + AddSchemaTableRow("Xml_Xml", null, null, null, false, false, false, SqlDbType.Xml, null, null, null, null, BulkDataReaderTest.testXMLSchemaCollectionName); + AddSchemaTableRow("Xml", null, null, null, false, false, false, SqlDbType.Xml, null, null, null, null, null); + } + + /// + /// The result set returned by the . + /// + public static readonly ReadOnlyCollection ExpectedResultSet = new ReadOnlyCollection(new List + { + (long)10, + new byte[20], + true, + null, + 'c', + new char[] { 'c' }, + "c", + "char 20", + DateTime.UtcNow, + DateTime.UtcNow, + DateTime.UtcNow, + DateTime.UtcNow, + DateTimeOffset.UtcNow, + DateTimeOffset.UtcNow, + (decimal)10.5, + (double)10.5, + new byte[20], + (int)10, + (decimal)10.5, + "nchar 20", + "ntext", + "nvarchar 20", + "nvarchar max", + (float)10.5, + DateTime.UtcNow, + (short)10, + (decimal)10.5, + "text", + DateTime.UtcNow.TimeOfDay, + DateTime.UtcNow.TimeOfDay, + (byte)10, + new object(), + Guid.NewGuid(), + new byte[20], + new byte[20], + "varchar 20", + "varchar max", + (int)10, + @"", + @"", + @"", + @"", + @"" + }); + + /// + /// See + /// + /// + /// The zero-based column ordinal. + /// + /// + /// The value of the column in . + /// + /// + public override object GetValue(int i) + { + return BulkDataReaderSubclass.ExpectedResultSet[i]; + } + + /// + /// The number of rows read. + /// + private int readCount = 0; + + /// + /// See + /// + /// + /// True if there are more rows; otherwise, false. + /// + /// + public override bool Read() + { + return readCount++ < 1; + } + + #endregion + + } + + private class BulkDataReaderSchemaTest : BulkDataReader + { + + #region Properties + + /// + /// Is the column nullable (i.e. optional)? + /// + public bool AllowDBNull { get; set; } + + /// + /// The name of the column. + /// + public string ColumnName { get; set; } + + /// + /// The size of the column which may be null if not applicable. + /// + public int? ColumnSize { get; set; } + + /// + /// Is the column part of the primary key? + /// + public bool IsKey { get; set; } + + /// + /// Are the column values unique (i.e. never duplicated)? + /// + public bool IsUnique { get; set; } + + /// + /// The precision of the column which may be null if not applicable. + /// + public short? NumericPrecision { get; set; } + + /// + /// The scale of the column which may be null if not applicable. + /// + public short? NumericScale { get; set; } + + /// + /// The corresponding . + /// + public SqlDbType ProviderType { get; set; } + + /// + /// The schema name of the UDT. + /// + public string UdtSchema { get; set; } + + /// + /// The type name of the UDT. + /// + public string UdtType { get; set; } + + /// + /// For XML columns the schema collection's database name. Otherwise, null. + /// + public string XmlSchemaCollectionDatabase { get; set; } + + /// + /// For XML columns the schema collection's name. Otherwise, null. + /// + public string XmlSchemaCollectionName { get; set; } + + /// + /// For XML columns the schema collection's schema name. Otherwise, null. + /// + public string XmlSchemaCollectionOwningSchema { get; set; } + + #endregion + + #region Constructors + + /// + /// Constructor. + /// + public BulkDataReaderSchemaTest() + { + } + + #endregion + + #region BulkDataReader + + /// + /// See . + /// + /// + /// Returns . + /// + protected override string SchemaName + { + get { return BulkDataReaderTest.testSchemaName; } + } + + /// + /// See . + /// + /// + /// Returns . + /// + protected override string TableName + { + get { return BulkDataReaderTest.testTableName; } + } + + /// + /// See + /// + /// + /// Creates a schema row for the various values. + /// + protected override void AddSchemaTableRows() + { + AddSchemaTableRow(this.ColumnName, + this.ColumnSize, + this.NumericPrecision, + this.NumericScale, + this.IsUnique, + this.IsKey, + this.AllowDBNull, + this.ProviderType, + this.UdtSchema, + this.UdtType, + this.XmlSchemaCollectionDatabase, + this.XmlSchemaCollectionOwningSchema, + this.XmlSchemaCollectionName); + } + + /// + /// See + /// + /// + /// The test stub is only for testing schema functionality and behaves as if it has no rows. + /// + /// + /// The zero-based column ordinal. + /// + /// + /// Never returns. + /// + /// + public override object GetValue(int i) + { + throw new InvalidOperationException("No data."); + } + + + /// + /// See + /// + /// + /// False. + /// + /// + public override bool Read() + { + return false; + } + + #endregion + + } + + #endregion + } +} diff --git a/src/Umbraco.Tests/Persistence/PetaPocoCachesTest.cs b/src/Umbraco.Tests/Persistence/PetaPocoCachesTest.cs new file mode 100644 index 0000000000..de32e0704b --- /dev/null +++ b/src/Umbraco.Tests/Persistence/PetaPocoCachesTest.cs @@ -0,0 +1,198 @@ +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.Linq; +using System.Threading; +using NUnit.Framework; +using Umbraco.Core.Models; +using Umbraco.Core.Persistence; +using Umbraco.Core.Services; +using Umbraco.Tests.Services; +using Umbraco.Tests.TestHelpers; +using Umbraco.Tests.TestHelpers.Entities; + +namespace Umbraco.Tests.Persistence +{ + [DatabaseTestBehavior(DatabaseBehavior.NewDbFileAndSchemaPerTest)] + [TestFixture, NUnit.Framework.Ignore] + public class PetaPocoCachesTest : BaseServiceTest + { + +#if DEBUG + /// + /// This tests the peta poco caches + /// + /// + /// This test WILL fail. This is because we cannot stop PetaPoco from creating more cached items for queries such as + /// ContentTypeRepository.GetAll(1,2,3,4); + /// when combined with other GetAll queries that pass in an array of Ids, each query generated for different length + /// arrays will produce a unique query which then gets added to the cache. + /// + /// This test confirms this, if you analyze the DIFFERENCE output below you can see why the cached queries grow. + /// + [Test] + public void Check_Peta_Poco_Caches() + { + var result = new List>>(); + + Database.PocoData.UseLongKeys = true; + + for (int i = 0; i < 2; i++) + { + int id1, id2, id3; + string alias; + CreateStuff(out id1, out id2, out id3, out alias); + QueryStuff(id1, id2, id3, alias); + + double totalBytes1; + IEnumerable keys; + Debug.Print(Database.PocoData.PrintDebugCacheReport(out totalBytes1, out keys)); + + result.Add(new Tuple>(totalBytes1, keys.Count(), keys)); + } + + for (int index = 0; index < result.Count; index++) + { + var tuple = result[index]; + Debug.Print("Bytes: {0}, Delegates: {1}", tuple.Item1, tuple.Item2); + if (index != 0) + { + Debug.Print("----------------DIFFERENCE---------------------"); + var diff = tuple.Item3.Except(result[index - 1].Item3); + foreach (var d in diff) + { + Debug.Print(d); + } + } + + } + + var allByteResults = result.Select(x => x.Item1).Distinct(); + var totalKeys = result.Select(x => x.Item2).Distinct(); + + Assert.AreEqual(1, allByteResults.Count()); + Assert.AreEqual(1, totalKeys.Count()); + } + + [Test] + public void Verify_Memory_Expires() + { + Database.PocoData.SlidingExpirationSeconds = 2; + + var managedCache = new Database.ManagedCache(); + + int id1, id2, id3; + string alias; + CreateStuff(out id1, out id2, out id3, out alias); + QueryStuff(id1, id2, id3, alias); + + var count1 = managedCache.GetCache().GetCount(); + Debug.Print("Keys = " + count1); + Assert.Greater(count1, 0); + + Thread.Sleep(10000); + + var count2 = managedCache.GetCache().GetCount(); + Debug.Print("Keys = " + count2); + Assert.Less(count2, count1); + } + + private void QueryStuff(int id1, int id2, int id3, string alias1) + { + var contentService = ServiceContext.ContentService; + + ServiceContext.TagService.GetTagsForEntity(id1); + + ServiceContext.TagService.GetAllContentTags(); + + ServiceContext.TagService.GetTagsForEntity(id2); + + ServiceContext.TagService.GetTagsForEntity(id3); + + contentService.CountDescendants(id3); + + contentService.CountChildren(id3); + + contentService.Count(contentTypeAlias: alias1); + + contentService.Count(); + + contentService.GetById(Guid.NewGuid()); + + contentService.GetByLevel(2); + + contentService.GetChildren(id1); + + contentService.GetDescendants(id2); + + contentService.GetVersions(id3); + + contentService.GetRootContent(); + + contentService.GetContentForExpiration(); + + contentService.GetContentForRelease(); + + contentService.GetContentInRecycleBin(); + + ((ContentService)contentService).GetPublishedDescendants(new Content("Test", -1, new ContentType(-1)) + { + Id = id1, + Path = "-1," + id1 + }); + + contentService.GetByVersion(Guid.NewGuid()); + } + + private void CreateStuff(out int id1, out int id2, out int id3, out string alias) + { + var contentService = ServiceContext.ContentService; + + var ctAlias = "umbTextpage" + Guid.NewGuid().ToString("N"); + alias = ctAlias; + + for (int i = 0; i < 20; i++) + { + contentService.CreateContentWithIdentity("Test", -1, "umbTextpage", 0); + } + var contentTypeService = ServiceContext.ContentTypeService; + var contentType = MockedContentTypes.CreateSimpleContentType(ctAlias, "test Doc Type"); + contentTypeService.Save(contentType); + for (int i = 0; i < 20; i++) + { + contentService.CreateContentWithIdentity("Test", -1, ctAlias, 0); + } + var parent = contentService.CreateContentWithIdentity("Test", -1, ctAlias, 0); + id1 = parent.Id; + + for (int i = 0; i < 20; i++) + { + contentService.CreateContentWithIdentity("Test", parent, ctAlias); + } + IContent current = parent; + for (int i = 0; i < 20; i++) + { + current = contentService.CreateContentWithIdentity("Test", current, ctAlias); + } + contentType = MockedContentTypes.CreateSimpleContentType("umbMandatory" + Guid.NewGuid().ToString("N"), "Mandatory Doc Type", true); + contentType.PropertyGroups.First().PropertyTypes.Add( + new PropertyType("test", DataTypeDatabaseType.Ntext, "tags") + { + DataTypeDefinitionId = 1041 + }); + contentTypeService.Save(contentType); + var content1 = MockedContent.CreateSimpleContent(contentType, "Tagged content 1", -1); + content1.SetTags("tags", new[] { "hello", "world", "some", "tags" }, true); + contentService.Publish(content1); + id2 = content1.Id; + + var content2 = MockedContent.CreateSimpleContent(contentType, "Tagged content 2", -1); + content2.SetTags("tags", new[] { "hello", "world", "some", "tags" }, true); + contentService.Publish(content2); + id3 = content2.Id; + + contentService.MoveToRecycleBin(content1); + } +#endif + } +} \ No newline at end of file diff --git a/src/Umbraco.Tests/Persistence/PetaPocoExtensionsTest.cs b/src/Umbraco.Tests/Persistence/PetaPocoExtensionsTest.cs index 0a15994785..d06d422d83 100644 --- a/src/Umbraco.Tests/Persistence/PetaPocoExtensionsTest.cs +++ b/src/Umbraco.Tests/Persistence/PetaPocoExtensionsTest.cs @@ -1,201 +1,16 @@ using System; using System.Collections.Generic; -using System.Diagnostics; -using System.Linq; using System.Text.RegularExpressions; -using System.Threading; using NUnit.Framework; using Umbraco.Core; -using Umbraco.Core.Models; +using Umbraco.Core.Logging; using Umbraco.Core.Models.Rdbms; using Umbraco.Core.Persistence; -using Umbraco.Core.Services; -using Umbraco.Tests.Services; +using Umbraco.Core.Persistence.SqlSyntax; using Umbraco.Tests.TestHelpers; -using Umbraco.Tests.TestHelpers.Entities; namespace Umbraco.Tests.Persistence { - [DatabaseTestBehavior(DatabaseBehavior.NewDbFileAndSchemaPerTest)] - [TestFixture, NUnit.Framework.Ignore] - public class PetaPocoCachesTest : BaseServiceTest - { - /// - /// This tests the peta poco caches - /// - /// - /// This test WILL fail. This is because we cannot stop PetaPoco from creating more cached items for queries such as - /// ContentTypeRepository.GetAll(1,2,3,4); - /// when combined with other GetAll queries that pass in an array of Ids, each query generated for different length - /// arrays will produce a unique query which then gets added to the cache. - /// - /// This test confirms this, if you analyze the DIFFERENCE output below you can see why the cached queries grow. - /// - [Test] - public void Check_Peta_Poco_Caches() - { - var result = new List>>(); - - Database.PocoData.UseLongKeys = true; - - for (int i = 0; i < 2; i++) - { - int id1, id2, id3; - string alias; - CreateStuff(out id1, out id2, out id3, out alias); - QueryStuff(id1, id2, id3, alias); - - double totalBytes1; - IEnumerable keys; - Debug.Print(Database.PocoData.PrintDebugCacheReport(out totalBytes1, out keys)); - - result.Add(new Tuple>(totalBytes1, keys.Count(), keys)); - } - - for (int index = 0; index < result.Count; index++) - { - var tuple = result[index]; - Debug.Print("Bytes: {0}, Delegates: {1}", tuple.Item1, tuple.Item2); - if (index != 0) - { - Debug.Print("----------------DIFFERENCE---------------------"); - var diff = tuple.Item3.Except(result[index - 1].Item3); - foreach (var d in diff) - { - Debug.Print(d); - } - } - - } - - var allByteResults = result.Select(x => x.Item1).Distinct(); - var totalKeys = result.Select(x => x.Item2).Distinct(); - - Assert.AreEqual(1, allByteResults.Count()); - Assert.AreEqual(1, totalKeys.Count()); - } - - [Test] - public void Verify_Memory_Expires() - { - Database.PocoData.SlidingExpirationSeconds = 2; - - var managedCache = new Database.ManagedCache(); - - int id1, id2, id3; - string alias; - CreateStuff(out id1, out id2, out id3, out alias); - QueryStuff(id1, id2, id3, alias); - - var count1 = managedCache.GetCache().GetCount(); - Debug.Print("Keys = " + count1); - Assert.Greater(count1, 0); - - Thread.Sleep(10000); - - var count2 = managedCache.GetCache().GetCount(); - Debug.Print("Keys = " + count2); - Assert.Less(count2, count1); - } - - private void QueryStuff(int id1, int id2, int id3, string alias1) - { - var contentService = ServiceContext.ContentService; - - ServiceContext.TagService.GetTagsForEntity(id1); - - ServiceContext.TagService.GetAllContentTags(); - - ServiceContext.TagService.GetTagsForEntity(id2); - - ServiceContext.TagService.GetTagsForEntity(id3); - - contentService.CountDescendants(id3); - - contentService.CountChildren(id3); - - contentService.Count(contentTypeAlias: alias1); - - contentService.Count(); - - contentService.GetById(Guid.NewGuid()); - - contentService.GetByLevel(2); - - contentService.GetChildren(id1); - - contentService.GetDescendants(id2); - - contentService.GetVersions(id3); - - contentService.GetRootContent(); - - contentService.GetContentForExpiration(); - - contentService.GetContentForRelease(); - - contentService.GetContentInRecycleBin(); - - ((ContentService)contentService).GetPublishedDescendants(new Content("Test", -1, new ContentType(-1)) - { - Id = id1, - Path = "-1," + id1 - }); - - contentService.GetByVersion(Guid.NewGuid()); - } - - private void CreateStuff(out int id1, out int id2, out int id3, out string alias) - { - var contentService = ServiceContext.ContentService; - - var ctAlias = "umbTextpage" + Guid.NewGuid().ToString("N"); - alias = ctAlias; - - for (int i = 0; i < 20; i++) - { - contentService.CreateContentWithIdentity("Test", -1, "umbTextpage", 0); - } - var contentTypeService = ServiceContext.ContentTypeService; - var contentType = MockedContentTypes.CreateSimpleContentType(ctAlias, "test Doc Type"); - contentTypeService.Save(contentType); - for (int i = 0; i < 20; i++) - { - contentService.CreateContentWithIdentity("Test", -1, ctAlias, 0); - } - var parent = contentService.CreateContentWithIdentity("Test", -1, ctAlias, 0); - id1 = parent.Id; - - for (int i = 0; i < 20; i++) - { - contentService.CreateContentWithIdentity("Test", parent, ctAlias); - } - IContent current = parent; - for (int i = 0; i < 20; i++) - { - current = contentService.CreateContentWithIdentity("Test", current, ctAlias); - } - contentType = MockedContentTypes.CreateSimpleContentType("umbMandatory" + Guid.NewGuid().ToString("N"), "Mandatory Doc Type", true); - contentType.PropertyGroups.First().PropertyTypes.Add( - new PropertyType("test", DataTypeDatabaseType.Ntext, "tags") - { - DataTypeDefinitionId = 1041 - }); - contentTypeService.Save(contentType); - var content1 = MockedContent.CreateSimpleContent(contentType, "Tagged content 1", -1); - content1.SetTags("tags", new[] { "hello", "world", "some", "tags" }, true); - contentService.Publish(content1); - id2 = content1.Id; - - var content2 = MockedContent.CreateSimpleContent(contentType, "Tagged content 2", -1); - content2.SetTags("tags", new[] { "hello", "world", "some", "tags" }, true); - contentService.Publish(content2); - id3 = content2.Id; - - contentService.MoveToRecycleBin(content1); - } - } - [DatabaseTestBehavior(DatabaseBehavior.NewDbFileAndSchemaPerTest)] [TestFixture] public class PetaPocoExtensionsTest : BaseDatabaseFactoryTest @@ -213,7 +28,7 @@ namespace Umbraco.Tests.Persistence } [Test] - public void Can_Bulk_Insert() + public void Can_Bulk_Insert_One_By_One() { // Arrange var db = DatabaseContext.Database; @@ -234,13 +49,168 @@ namespace Umbraco.Tests.Persistence // Act using (ProfilingLogger.TraceDuration("starting insert", "finished insert")) { - db.BulkInsertRecords(servers); + using (var tr = db.GetTransaction()) + { + db.BulkInsertRecords(servers, tr, SqlSyntax, useNativeSqlPlatformBulkInsert:false); + tr.Complete(); + } } // Assert Assert.That(db.ExecuteScalar("SELECT COUNT(*) FROM umbracoServer"), Is.EqualTo(1000)); } + [Test] + public void Can_Bulk_Insert_One_By_One_Transaction_Rollback() + { + // Arrange + var db = DatabaseContext.Database; + + var servers = new List(); + for (var i = 0; i < 1000; i++) + { + servers.Add(new ServerRegistrationDto + { + ServerAddress = "address" + i, + ServerIdentity = "computer" + i, + DateRegistered = DateTime.Now, + IsActive = true, + DateAccessed = DateTime.Now + }); + } + + // Act + using (ProfilingLogger.TraceDuration("starting insert", "finished insert")) + { + using (var tr = db.GetTransaction()) + { + db.BulkInsertRecords(servers, tr, SqlSyntax, useNativeSqlPlatformBulkInsert: false); + //don't call complete here - the trans will be rolled back + } + } + + // Assert + Assert.That(db.ExecuteScalar("SELECT COUNT(*) FROM umbracoServer"), Is.EqualTo(0)); + } + + + [NUnit.Framework.Ignore("Ignored because you need to configure your own SQL Server to test thsi with")] + [Test] + public void Can_Bulk_Insert_Native_Sql_Server_Bulk_Inserts() + { + //create the db + var dbSqlServer = new UmbracoDatabase( + "server=.\\SQLExpress;database=YOURDB;user id=YOURUSER;password=YOURPASSWORD", + Constants.DatabaseProviders.SqlServer, + new DebugDiagnosticsLogger()); + + //drop the table + dbSqlServer.Execute("DROP TABLE [umbracoServer]"); + + //re-create it + dbSqlServer.Execute(@"CREATE TABLE [umbracoServer]( + [id] [int] IDENTITY(1,1) NOT NULL, + [address] [nvarchar](500) NOT NULL, + [computerName] [nvarchar](255) NOT NULL, + [registeredDate] [datetime] NOT NULL CONSTRAINT [DF_umbracoServer_registeredDate] DEFAULT (getdate()), + [lastNotifiedDate] [datetime] NOT NULL, + [isActive] [bit] NOT NULL, + [isMaster] [bit] NOT NULL, + CONSTRAINT [PK_umbracoServer] PRIMARY KEY CLUSTERED +( + [id] ASC +)WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF, IGNORE_DUP_KEY = OFF, ALLOW_ROW_LOCKS = ON, ALLOW_PAGE_LOCKS = ON) ON [PRIMARY] +)"); + var data = new List(); + for (var i = 0; i < 1000; i++) + { + data.Add(new ServerRegistrationDto + { + ServerAddress = "address" + i, + ServerIdentity = "computer" + i, + DateRegistered = DateTime.Now, + IsActive = true, + DateAccessed = DateTime.Now + }); + } + + var sqlServerSyntax = new SqlServerSyntaxProvider(); + using (var tr = dbSqlServer.GetTransaction()) + { + dbSqlServer.BulkInsertRecords(data, tr, sqlServerSyntax, useNativeSqlPlatformBulkInsert: true); + tr.Complete(); + } + + // Assert + Assert.That(dbSqlServer.ExecuteScalar("SELECT COUNT(*) FROM umbracoServer"), Is.EqualTo(1000)); + } + + [Test] + public void Can_Bulk_Insert_Native_Sql_Bulk_Inserts() + { + // Arrange + var db = DatabaseContext.Database; + + var servers = new List(); + for (var i = 0; i < 1000; i++) + { + servers.Add(new ServerRegistrationDto + { + ServerAddress = "address" + i, + ServerIdentity = "computer" + i, + DateRegistered = DateTime.Now, + IsActive = true, + DateAccessed = DateTime.Now + }); + } + + // Act + using (ProfilingLogger.TraceDuration("starting insert", "finished insert")) + { + using (var tr = db.GetTransaction()) + { + db.BulkInsertRecords(servers, tr, SqlSyntax, useNativeSqlPlatformBulkInsert: true); + tr.Complete(); + } + } + + // Assert + Assert.That(db.ExecuteScalar("SELECT COUNT(*) FROM umbracoServer"), Is.EqualTo(1000)); + } + + [Test] + public void Can_Bulk_Insert_Native_Sql_Bulk_Inserts_Transaction_Rollback() + { + // Arrange + var db = DatabaseContext.Database; + + var servers = new List(); + for (var i = 0; i < 1000; i++) + { + servers.Add(new ServerRegistrationDto + { + ServerAddress = "address" + i, + ServerIdentity = "computer" + i, + DateRegistered = DateTime.Now, + IsActive = true, + DateAccessed = DateTime.Now + }); + } + + // Act + using (ProfilingLogger.TraceDuration("starting insert", "finished insert")) + { + using (var tr = db.GetTransaction()) + { + db.BulkInsertRecords(servers, tr, SqlSyntax, useNativeSqlPlatformBulkInsert: true); + //don't call complete here - the trans will be rolled back + } + } + + // Assert + Assert.That(db.ExecuteScalar("SELECT COUNT(*) FROM umbracoServer"), Is.EqualTo(0)); + } + [Test] public void Generate_Bulk_Import_Sql() { @@ -263,7 +233,9 @@ namespace Umbraco.Tests.Persistence // Act string[] sql; - db.GenerateBulkInsertCommand(servers, db.Connection, out sql); + db.GenerateBulkInsertCommand( + Database.PocoData.ForType(typeof(ServerRegistrationDto)), + servers, out sql); db.CloseSharedConnection(); // Assert @@ -295,7 +267,7 @@ namespace Umbraco.Tests.Persistence // Act string[] sql; - db.GenerateBulkInsertCommand(servers, db.Connection, out sql); + db.GenerateBulkInsertCommand(Database.PocoData.ForType(typeof(ServerRegistrationDto)), servers, out sql); db.CloseSharedConnection(); // Assert diff --git a/src/Umbraco.Tests/Persistence/SyntaxProvider/MySqlSyntaxProviderTests.cs b/src/Umbraco.Tests/Persistence/SyntaxProvider/MySqlSyntaxProviderTests.cs index 8126aa5e36..e58506aa03 100644 --- a/src/Umbraco.Tests/Persistence/SyntaxProvider/MySqlSyntaxProviderTests.cs +++ b/src/Umbraco.Tests/Persistence/SyntaxProvider/MySqlSyntaxProviderTests.cs @@ -28,7 +28,7 @@ namespace Umbraco.Tests.Persistence.SyntaxProvider public void Can_Generate_Create_Table_Statement() { var type = typeof(TagRelationshipDto); - var definition = DefinitionFactory.GetTableDefinition(type); + var definition = DefinitionFactory.GetTableDefinition(SqlSyntaxContext.SqlSyntaxProvider, type); string create = SqlSyntaxContext.SqlSyntaxProvider.Format(definition); string primaryKey = SqlSyntaxContext.SqlSyntaxProvider.FormatPrimaryKey(definition); diff --git a/src/Umbraco.Tests/Persistence/SyntaxProvider/SqlCeSyntaxProviderTests.cs b/src/Umbraco.Tests/Persistence/SyntaxProvider/SqlCeSyntaxProviderTests.cs index e960f50799..fafddb8dfd 100644 --- a/src/Umbraco.Tests/Persistence/SyntaxProvider/SqlCeSyntaxProviderTests.cs +++ b/src/Umbraco.Tests/Persistence/SyntaxProvider/SqlCeSyntaxProviderTests.cs @@ -51,7 +51,7 @@ WHERE (([umbracoNode].[nodeObjectType] = @0))) x)".Replace(Environment.NewLine, var sqlSyntax = new SqlCeSyntaxProvider(); var type = typeof (NodeDto); - var definition = DefinitionFactory.GetTableDefinition(type); + var definition = DefinitionFactory.GetTableDefinition(sqlSyntax, type); string create = sqlSyntax.Format(definition); string primaryKey = sqlSyntax.FormatPrimaryKey(definition); diff --git a/src/Umbraco.Tests/Umbraco.Tests.csproj b/src/Umbraco.Tests/Umbraco.Tests.csproj index 8659a73cd1..a606d401a0 100644 --- a/src/Umbraco.Tests/Umbraco.Tests.csproj +++ b/src/Umbraco.Tests/Umbraco.Tests.csproj @@ -176,7 +176,9 @@ + + From 1a6986cfcfac4610678077e07f39dcb5b5cea8dc Mon Sep 17 00:00:00 2001 From: Shannon Date: Tue, 25 Oct 2016 12:34:06 +0200 Subject: [PATCH 23/48] U4-9111 Benchmark BulkCopy changes from U4-9107 --- src/Umbraco.Core/Properties/AssemblyInfo.cs | 1 + src/Umbraco.Tests.Benchmarks/App.config | 48 ++++ .../BulkInsertBenchmarks.cs | 210 ++++++++++++++++++ src/Umbraco.Tests.Benchmarks/Program.cs | 19 ++ .../Properties/AssemblyInfo.cs | 36 +++ .../TraceEvent.ReleaseNotes.txt | 61 +++++ .../Umbraco.Tests.Benchmarks.csproj | 149 +++++++++++++ src/Umbraco.Tests.Benchmarks/packages.config | 25 +++ src/umbraco.sln | 9 +- 9 files changed, 557 insertions(+), 1 deletion(-) create mode 100644 src/Umbraco.Tests.Benchmarks/App.config create mode 100644 src/Umbraco.Tests.Benchmarks/BulkInsertBenchmarks.cs create mode 100644 src/Umbraco.Tests.Benchmarks/Program.cs create mode 100644 src/Umbraco.Tests.Benchmarks/Properties/AssemblyInfo.cs create mode 100644 src/Umbraco.Tests.Benchmarks/TraceEvent.ReleaseNotes.txt create mode 100644 src/Umbraco.Tests.Benchmarks/Umbraco.Tests.Benchmarks.csproj create mode 100644 src/Umbraco.Tests.Benchmarks/packages.config diff --git a/src/Umbraco.Core/Properties/AssemblyInfo.cs b/src/Umbraco.Core/Properties/AssemblyInfo.cs index d1ddadde37..dbc1ab6c93 100644 --- a/src/Umbraco.Core/Properties/AssemblyInfo.cs +++ b/src/Umbraco.Core/Properties/AssemblyInfo.cs @@ -32,6 +32,7 @@ using System.Security.Permissions; [assembly: InternalsVisibleTo("umbraco.editorControls")] [assembly: InternalsVisibleTo("Umbraco.Tests")] +[assembly: InternalsVisibleTo("Umbraco.Tests.Benchmarks")] [assembly: InternalsVisibleTo("Umbraco.Core")] [assembly: InternalsVisibleTo("Umbraco.Web")] [assembly: InternalsVisibleTo("Umbraco.Web.UI")] diff --git a/src/Umbraco.Tests.Benchmarks/App.config b/src/Umbraco.Tests.Benchmarks/App.config new file mode 100644 index 0000000000..a988966f23 --- /dev/null +++ b/src/Umbraco.Tests.Benchmarks/App.config @@ -0,0 +1,48 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/Umbraco.Tests.Benchmarks/BulkInsertBenchmarks.cs b/src/Umbraco.Tests.Benchmarks/BulkInsertBenchmarks.cs new file mode 100644 index 0000000000..5212a4f524 --- /dev/null +++ b/src/Umbraco.Tests.Benchmarks/BulkInsertBenchmarks.cs @@ -0,0 +1,210 @@ +using System; +using System.Collections.Generic; +using System.Data.SqlServerCe; +using System.Diagnostics; +using System.IO; +using System.Threading; +using System.Xml; +using BenchmarkDotNet.Attributes; +using BenchmarkDotNet.Columns; +using BenchmarkDotNet.Configs; +using BenchmarkDotNet.Diagnosers; +using BenchmarkDotNet.Diagnostics.Windows; +using BenchmarkDotNet.Jobs; +using BenchmarkDotNet.Loggers; +using BenchmarkDotNet.Reports; +using BenchmarkDotNet.Running; +using BenchmarkDotNet.Validators; +using Umbraco.Core; +using Umbraco.Core.Logging; +using Umbraco.Core.Models.Rdbms; +using Umbraco.Core.Persistence; +using Umbraco.Core.Persistence.Migrations.Initial; +using Umbraco.Core.Persistence.SqlSyntax; +using Umbraco.Tests.TestHelpers; +using ILogger = Umbraco.Core.Logging.ILogger; + +namespace Umbraco.Tests.Benchmarks +{ + [Config(typeof(Config))] + public class BulkInsertBenchmarks + { + private class Config : ManualConfig + { + public Config() + { + Add(new MemoryDiagnoser()); + //Add(ExecutionValidator.FailOnError); + + //The 'quick and dirty' settings, so it runs a little quicker + // see benchmarkdotnet FAQ + Add(Job.Default + .WithLaunchCount(1) // benchmark process will be launched only once + .WithIterationTime(100) // 100ms per iteration + .WithWarmupCount(3) // 3 warmup iteration + .WithTargetCount(3)); // 3 target iteration + } + } + + private static byte[] _initDbBytes = null; + + [Setup] + public void Setup() + { + var logger = new DebugDiagnosticsLogger(); + var path = TestHelper.CurrentAssemblyDirectory; + + _sqlCeSyntax = new SqlCeSyntaxProvider(); + _sqlServerSyntax = new SqlServerSyntaxProvider(); + + SetupSqlCe(path, logger); + SetupSqlServer(logger); + + + } + + private void SetupSqlServer(ILogger logger) + { + //create the db + _dbSqlServer = new UmbracoDatabase( + "server=.\\SQLExpress;database=YOURDB;user id=YOURUSER;password=YOURPASS", + Constants.DatabaseProviders.SqlServer, + logger); + + //drop the table + _dbSqlServer.Execute("DROP TABLE [umbracoServer]"); + + //re-create it + _dbSqlServer.Execute(@"CREATE TABLE [umbracoServer]( + [id] [int] IDENTITY(1,1) NOT NULL, + [address] [nvarchar](500) NOT NULL, + [computerName] [nvarchar](255) NOT NULL, + [registeredDate] [datetime] NOT NULL CONSTRAINT [DF_umbracoServer_registeredDate] DEFAULT (getdate()), + [lastNotifiedDate] [datetime] NOT NULL, + [isActive] [bit] NOT NULL, + [isMaster] [bit] NOT NULL, + CONSTRAINT [PK_umbracoServer] PRIMARY KEY CLUSTERED +( + [id] ASC +)WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF, IGNORE_DUP_KEY = OFF, ALLOW_ROW_LOCKS = ON, ALLOW_PAGE_LOCKS = ON) ON [PRIMARY] +)"); + } + + private void SetupSqlCe(string path, ILogger logger) + { + var dbName = string.Concat("Umb", Guid.NewGuid(), ".sdf"); + AppDomain.CurrentDomain.SetData("DataDirectory", path); + var sqlCeConnectionString = $"Datasource=|DataDirectory|\\{dbName};Flush Interval=1;"; + + _dbFile = Path.Combine(path, dbName); + + //only create the db one time + if (_initDbBytes == null) + { + using (var engine = new SqlCeEngine(sqlCeConnectionString)) + { + engine.CreateDatabase(); + } + + //use the db to create the initial schema so we can reuse in each bench + using (_dbSqlCe = new UmbracoDatabase( + sqlCeConnectionString, + Constants.DatabaseProviders.SqlCe, + logger)) + { + var creation = new DatabaseSchemaCreation(_dbSqlCe, logger, _sqlCeSyntax); + creation.InitializeDatabaseSchema(); + } + _initDbBytes = File.ReadAllBytes(_dbFile); + } + else + { + File.WriteAllBytes(_dbFile, _initDbBytes); + } + + //create the db + _dbSqlCe = new UmbracoDatabase( + sqlCeConnectionString, + Constants.DatabaseProviders.SqlCe, + logger); + } + + private List GetData() + { + var data = new List(); + for (var i = 0; i < 1000; i++) + { + data.Add(new ServerRegistrationDto + { + ServerAddress = "address" + Guid.NewGuid(), + ServerIdentity = "computer" + Guid.NewGuid(), + DateRegistered = DateTime.Now, + IsActive = true, + DateAccessed = DateTime.Now + }); + } + return data; + } + + [Cleanup] + public void Cleanup() + { + _dbSqlCe.Dispose(); + _dbSqlServer.Dispose(); + File.Delete(_dbFile); + } + + private string _dbFile; + private UmbracoDatabase _dbSqlCe; + private UmbracoDatabase _dbSqlServer; + private ISqlSyntaxProvider _sqlCeSyntax; + private ISqlSyntaxProvider _sqlServerSyntax; + + /// + /// Tests updating the existing XML way + /// + [Benchmark(Baseline = true)] + public void SqlCeOneByOne() + { + using (var tr = _dbSqlCe.GetTransaction()) + { + _dbSqlCe.BulkInsertRecords(GetData(), tr, _sqlCeSyntax, useNativeSqlPlatformBulkInsert: false); + tr.Complete(); + } + } + + /// + /// Tests updating with only the object graph + /// + [Benchmark] + public void SqlCeTableDirect() + { + using (var tr = _dbSqlCe.GetTransaction()) + { + _dbSqlCe.BulkInsertRecords(GetData(), tr, _sqlCeSyntax, useNativeSqlPlatformBulkInsert: true); + tr.Complete(); + } + } + + [Benchmark] + public void SqlServerBulkInsertStatements() + { + using (var tr = _dbSqlServer.GetTransaction()) + { + _dbSqlServer.BulkInsertRecords(GetData(), tr, _sqlServerSyntax, useNativeSqlPlatformBulkInsert: false); + tr.Complete(); + } + } + + [Benchmark] + public void SqlServerBulkCopy() + { + using (var tr = _dbSqlServer.GetTransaction()) + { + _dbSqlServer.BulkInsertRecords(GetData(), tr, _sqlServerSyntax, useNativeSqlPlatformBulkInsert: true); + tr.Complete(); + } + } + + } +} \ No newline at end of file diff --git a/src/Umbraco.Tests.Benchmarks/Program.cs b/src/Umbraco.Tests.Benchmarks/Program.cs new file mode 100644 index 0000000000..37c1ccd853 --- /dev/null +++ b/src/Umbraco.Tests.Benchmarks/Program.cs @@ -0,0 +1,19 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using BenchmarkDotNet.Running; + +namespace Umbraco.Tests.Benchmarks +{ + class Program + { + static void Main(string[] args) + { + var summary = BenchmarkRunner.Run(); + + Console.ReadLine(); + } + } +} diff --git a/src/Umbraco.Tests.Benchmarks/Properties/AssemblyInfo.cs b/src/Umbraco.Tests.Benchmarks/Properties/AssemblyInfo.cs new file mode 100644 index 0000000000..fb00b8d4e9 --- /dev/null +++ b/src/Umbraco.Tests.Benchmarks/Properties/AssemblyInfo.cs @@ -0,0 +1,36 @@ +using System.Reflection; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; + +// General Information about an assembly is controlled through the following +// set of attributes. Change these attribute values to modify the information +// associated with an assembly. +[assembly: AssemblyTitle("Umbraco.Tests.Benchmarks")] +[assembly: AssemblyDescription("")] +[assembly: AssemblyConfiguration("")] +[assembly: AssemblyCompany("")] +[assembly: AssemblyProduct("Umbraco.Tests.Benchmarks")] +[assembly: AssemblyCopyright("Copyright © 2016")] +[assembly: AssemblyTrademark("")] +[assembly: AssemblyCulture("")] + +// Setting ComVisible to false makes the types in this assembly not visible +// to COM components. If you need to access a type in this assembly from +// COM, set the ComVisible attribute to true on that type. +[assembly: ComVisible(false)] + +// The following GUID is for the ID of the typelib if this project is exposed to COM +[assembly: Guid("86deb346-089f-4106-89c8-d852b9cf2a33")] + +// Version information for an assembly consists of the following four values: +// +// Major Version +// Minor Version +// Build Number +// Revision +// +// You can specify all the values or you can default the Build and Revision Numbers +// by using the '*' as shown below: +// [assembly: AssemblyVersion("1.0.*")] +[assembly: AssemblyVersion("1.0.0.0")] +[assembly: AssemblyFileVersion("1.0.0.0")] diff --git a/src/Umbraco.Tests.Benchmarks/TraceEvent.ReleaseNotes.txt b/src/Umbraco.Tests.Benchmarks/TraceEvent.ReleaseNotes.txt new file mode 100644 index 0000000000..21fcb5d0ca --- /dev/null +++ b/src/Umbraco.Tests.Benchmarks/TraceEvent.ReleaseNotes.txt @@ -0,0 +1,61 @@ +Version 1.0.0.3 - Initial release to NuGet, pre-release. + + TraceEvent has been available from the site http://bcl.codeplex.com/wikipage?title=TraceEvent for some time now + this NuGet Version of the library supersedes that one. WHile the 'core' part of the library is unchanged, + we did change lesser used features, and change the namespace and DLL name, which will cause break. We anticipate + it will take an hour or so to 'port' to this version from the old one. Below are specific details on what + has changed to help in this port. + + * The DLL has been renamed from TraceEvent.dll to Microsoft.Diagnostics.Tracing.TraceEvent.dll + * The name spaces for all classes have been changed. The easiest way to port is to simply place + the following using clauses at the top of any file that uses TraceEvent classes + using Microsoft.Diagnostics.Symbols; + using Microsoft.Diagnostics.Tracing; + using Microsoft.Diagnostics.Tracing.Etlx; + using Microsoft.Diagnostics.Tracing.Parsers.Clr; + using Microsoft.Diagnostics.Tracing.Parsers.Kernel; + using Microsoft.Diagnostics.Tracing.Session; + using Microsoft.Diagnostics.Tracing.Stacks; + * Any method with the name RelMSec in it has been changed to be RelativeMSec. The easiest port is to + simply globally rename RelMSec to RelativeMSec + * Any property in the Trace* classes that has the form Max*Index has been renamed to Count. + * A number of methods have been declared obsolete, these are mostly renames and the warning will tell you + how to update them. + * The following classes have been rename + SymPath -> SymbolPath + SymPathElement -> SymbolPathElement + SymbolReaderFlags -> SymbolReaderOptions + * TraceEventSession is now StopOnDispose (it will stop the session when TraceEventSesssion dies), by default + If you were relying on the kernel session living past the process that started it, you must now set + the StopOnDispose explicitly + * There used to be XmlAttrib extensions methods on StringBuilder for use in manifest generated TraceEventParsers + These have been moved to protected members of TraceEvent. The result is that in stead of writing + sb.XmlAttrib(...) you write XmlAttrib(sb, ...) + * References to Pdb in names have been replaced with 'Symbol' to conform to naming guidelines. + + *********************************************************************************************** +Version 1.0.0.4 - Initial stable release + + Mostly this was insuring that the library was cleaned up in preparation + for release the TraceParserGen tool + + Improved the docs, removed old code, fixed some naming convention stuff + + * Additional changes from the PreRelease copy to the first Stable release + + * The arguments to AddCallbackForProviderEvent were reversed!!!! (now provider than event) + * The arguments to Observe(string, string)!!!! (now provider than event) + * Event names for these APIs must include a / between the Task and Opcode names + + * Many Events in KernelTraceEventParser were harmonized to be consistent with other conventions + * Events of the form PageFault* were typically renamed to Memory* + * The 'End' suffix was renamed to 'Stop' (its official name) + * PerfInfoSampleProf -> PerfInfoSample + * PerfInfoSampleProf -> PerfInfoSample + * ReadyThread -> DispatcherReadyThread + * StackWalkTraceData -> StackWalkStackTraceData + * FileIo -> FileIO + * DiskIo -> DiskIO + + * Many Events in SymbolTraceEventParser were harmonized to be consistent with other conventions + * names with Symbol -> ImageID diff --git a/src/Umbraco.Tests.Benchmarks/Umbraco.Tests.Benchmarks.csproj b/src/Umbraco.Tests.Benchmarks/Umbraco.Tests.Benchmarks.csproj new file mode 100644 index 0000000000..66033ba08e --- /dev/null +++ b/src/Umbraco.Tests.Benchmarks/Umbraco.Tests.Benchmarks.csproj @@ -0,0 +1,149 @@ + + + + + Debug + AnyCPU + {86DEB346-089F-4106-89C8-D852B9CF2A33} + Exe + Properties + Umbraco.Tests.Benchmarks + Umbraco.Tests.Benchmarks + v4.5 + 512 + + + + + AnyCPU + true + full + false + bin\Debug\ + DEBUG;TRACE + prompt + 4 + + + AnyCPU + pdbonly + true + bin\Release\ + TRACE + prompt + 4 + + + Always + + + + ..\packages\BenchmarkDotNet.0.9.9\lib\net45\BenchmarkDotNet.dll + True + + + ..\packages\BenchmarkDotNet.Core.0.9.9\lib\net45\BenchmarkDotNet.Core.dll + True + + + ..\packages\BenchmarkDotNet.Diagnostics.Windows.0.9.9\lib\net45\BenchmarkDotNet.Diagnostics.Windows.dll + True + + + ..\packages\BenchmarkDotNet.Toolchains.Roslyn.0.9.9\lib\net45\BenchmarkDotNet.Toolchains.Roslyn.dll + True + + + ..\packages\Microsoft.CodeAnalysis.Common.1.3.2\lib\net45\Microsoft.CodeAnalysis.dll + True + + + ..\packages\Microsoft.CodeAnalysis.CSharp.1.3.2\lib\net45\Microsoft.CodeAnalysis.CSharp.dll + True + + + ..\packages\Microsoft.Diagnostics.Tracing.TraceEvent.1.0.41\lib\net40\Microsoft.Diagnostics.Tracing.TraceEvent.dll + True + + + + ..\packages\System.Collections.Immutable.1.1.37\lib\dotnet\System.Collections.Immutable.dll + True + + + + ..\packages\SqlServerCE.4.0.0.1\lib\System.Data.SqlServerCe.dll + True + + + ..\packages\SqlServerCE.4.0.0.1\lib\System.Data.SqlServerCe.Entity.dll + True + + + + ..\packages\System.Reflection.Metadata.1.2.0\lib\portable-net45+win8\System.Reflection.Metadata.dll + True + + + ..\packages\System.Threading.Tasks.Extensions.4.0.0\lib\portable-net45+win8+wp8+wpa81\System.Threading.Tasks.Extensions.dll + True + + + + + + + + + + + + + + + + + + + + + + + + + + + + + {31785bc3-256c-4613-b2f5-a1b0bdded8c1} + Umbraco.Core + + + {5d3b8245-ada6-453f-a008-50ed04bfe770} + Umbraco.Tests + + + + + + + This project references NuGet package(s) that are missing on this computer. Use NuGet Package Restore to download them. For more information, see http://go.microsoft.com/fwlink/?LinkID=322105. The missing file is {0}. + + + + + + + + + xcopy "$(ProjectDir)"..\packages\SqlServerCE.4.0.0.1\amd64\*.* "$(TargetDir)amd64\" /Y /F /E /I /C /D +xcopy "$(ProjectDir)"..\packages\SqlServerCE.4.0.0.1\x86\*.* "$(TargetDir)x86\" /Y /F /E /I /C /D + + + \ No newline at end of file diff --git a/src/Umbraco.Tests.Benchmarks/packages.config b/src/Umbraco.Tests.Benchmarks/packages.config new file mode 100644 index 0000000000..c4d2ba1df2 --- /dev/null +++ b/src/Umbraco.Tests.Benchmarks/packages.config @@ -0,0 +1,25 @@ + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/umbraco.sln b/src/umbraco.sln index f9267c0412..8648f6ed5c 100644 --- a/src/umbraco.sln +++ b/src/umbraco.sln @@ -1,6 +1,6 @@ Microsoft Visual Studio Solution File, Format Version 12.00 # Visual Studio 14 -VisualStudioVersion = 14.0.25123.0 +VisualStudioVersion = 14.0.25420.1 MinimumVisualStudioVersion = 10.0.40219.1 Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Umbraco.Web.UI", "Umbraco.Web.UI\Umbraco.Web.UI.csproj", "{4C4C194C-B5E4-4991-8F87-4373E24CC19F}" EndProject @@ -114,6 +114,8 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "build", "build", "{5B03EF4E ..\build\NuSpecs\build\UmbracoCms.targets = ..\build\NuSpecs\build\UmbracoCms.targets EndProjectSection EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Umbraco.Tests.Benchmarks", "Umbraco.Tests.Benchmarks\Umbraco.Tests.Benchmarks.csproj", "{86DEB346-089F-4106-89C8-D852B9CF2A33}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -178,6 +180,10 @@ Global {07FBC26B-2927-4A22-8D96-D644C667FECC}.Debug|Any CPU.Build.0 = Debug|Any CPU {07FBC26B-2927-4A22-8D96-D644C667FECC}.Release|Any CPU.ActiveCfg = Release|Any CPU {07FBC26B-2927-4A22-8D96-D644C667FECC}.Release|Any CPU.Build.0 = Release|Any CPU + {86DEB346-089F-4106-89C8-D852B9CF2A33}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {86DEB346-089F-4106-89C8-D852B9CF2A33}.Debug|Any CPU.Build.0 = Debug|Any CPU + {86DEB346-089F-4106-89C8-D852B9CF2A33}.Release|Any CPU.ActiveCfg = Release|Any CPU + {86DEB346-089F-4106-89C8-D852B9CF2A33}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -187,5 +193,6 @@ Global {5D3B8245-ADA6-453F-A008-50ED04BFE770} = {B5BD12C1-A454-435E-8A46-FF4A364C0382} {E3F9F378-AFE1-40A5-90BD-82833375DBFE} = {227C3B55-80E5-4E7E-A802-BE16C5128B9D} {5B03EF4E-E0AC-4905-861B-8C3EC1A0D458} = {227C3B55-80E5-4E7E-A802-BE16C5128B9D} + {86DEB346-089F-4106-89C8-D852B9CF2A33} = {B5BD12C1-A454-435E-8A46-FF4A364C0382} EndGlobalSection EndGlobal From e330d40005eab2cc4eda67fe4f1224dd7f8c7e65 Mon Sep 17 00:00:00 2001 From: Stephan Date: Tue, 25 Oct 2016 12:40:27 +0200 Subject: [PATCH 24/48] U4-9090 - Upgrade to ModelsBuilder 3.0.5 (reuse compiled live dll on restart) --- build/NuSpecs/UmbracoCms.nuspec | 2 +- src/Umbraco.Web.UI/Umbraco.Web.UI.csproj | 4 ++-- src/Umbraco.Web.UI/config/Dashboard.config | 20 ++++++++++---------- src/Umbraco.Web.UI/packages.config | 2 +- 4 files changed, 14 insertions(+), 14 deletions(-) diff --git a/build/NuSpecs/UmbracoCms.nuspec b/build/NuSpecs/UmbracoCms.nuspec index 54d12f6844..d30a139adc 100644 --- a/build/NuSpecs/UmbracoCms.nuspec +++ b/build/NuSpecs/UmbracoCms.nuspec @@ -17,7 +17,7 @@ - + diff --git a/src/Umbraco.Web.UI/Umbraco.Web.UI.csproj b/src/Umbraco.Web.UI/Umbraco.Web.UI.csproj index 234b3a8f09..6f2728a241 100644 --- a/src/Umbraco.Web.UI/Umbraco.Web.UI.csproj +++ b/src/Umbraco.Web.UI/Umbraco.Web.UI.csproj @@ -347,8 +347,8 @@ umbraco.providers - - ..\packages\Umbraco.ModelsBuilder.3.0.4\lib\Umbraco.ModelsBuilder.dll + + ..\packages\Umbraco.ModelsBuilder.3.0.5\lib\Umbraco.ModelsBuilder.dll True diff --git a/src/Umbraco.Web.UI/config/Dashboard.config b/src/Umbraco.Web.UI/config/Dashboard.config index 81bd9be6e8..c40218c8d2 100644 --- a/src/Umbraco.Web.UI/config/Dashboard.config +++ b/src/Umbraco.Web.UI/config/Dashboard.config @@ -99,16 +99,6 @@ -
- - developer - - - - /App_Plugins/ModelsBuilder/modelsbuilder.htm - - -
content @@ -119,4 +109,14 @@
+
+ + developer + + + + /App_Plugins/ModelsBuilder/modelsbuilder.htm + + +
diff --git a/src/Umbraco.Web.UI/packages.config b/src/Umbraco.Web.UI/packages.config index 50b4e59bdc..3850961ea8 100644 --- a/src/Umbraco.Web.UI/packages.config +++ b/src/Umbraco.Web.UI/packages.config @@ -35,6 +35,6 @@ - + \ No newline at end of file From f1d5ccb1e33f0f25f2e9b16ef70f50efe3a4606e Mon Sep 17 00:00:00 2001 From: Mads Rasmussen Date: Tue, 25 Oct 2016 15:40:31 +0200 Subject: [PATCH 25/48] Fixes: U4-9039 No progress indication when uploading local package --- .../views/install-local.controller.js | 3 ++ .../views/packager/views/install-local.html | 39 +++++++++++++++++-- 2 files changed, 39 insertions(+), 3 deletions(-) diff --git a/src/Umbraco.Web.UI.Client/src/views/packager/views/install-local.controller.js b/src/Umbraco.Web.UI.Client/src/views/packager/views/install-local.controller.js index e29df0b9ae..e5fba69a88 100644 --- a/src/Umbraco.Web.UI.Client/src/views/packager/views/install-local.controller.js +++ b/src/Umbraco.Web.UI.Client/src/views/packager/views/install-local.controller.js @@ -34,6 +34,9 @@ file: file }).progress(function (evt) { + // set view state to uploading + vm.state = 'uploading'; + // calculate progress in percentage var progressPercentage = parseInt(100.0 * evt.loaded / evt.total, 10); diff --git a/src/Umbraco.Web.UI.Client/src/views/packager/views/install-local.html b/src/Umbraco.Web.UI.Client/src/views/packager/views/install-local.html index 07fb21d00b..499e844588 100644 --- a/src/Umbraco.Web.UI.Client/src/views/packager/views/install-local.html +++ b/src/Umbraco.Web.UI.Client/src/views/packager/views/install-local.html @@ -37,10 +37,8 @@ - or click here to choose files -
- {{vm.zipFile.serverErrorMessage}} -
+ @@ -53,6 +51,41 @@ +
+ + + + ← Upload another package + + + +
+ +
+
+
+ +
+
+ +

Uploading package

+ + + + +
+ {{ vm.zipFile.serverErrorMessage }} +
+ +
+
+
+ +
+ +
+
From d7d4b26a663875c3071e8c14bc07bb896f9d7b25 Mon Sep 17 00:00:00 2001 From: Shannon Date: Tue, 25 Oct 2016 15:50:33 +0200 Subject: [PATCH 26/48] This changes the instruction fetch process to query for the top records based on the MaxProcessingInstructionCount. This changes the Fetch to be a Query so they are not all loaded into memory and instead uses a db reader. This checks if any JSON blob contains more than the MaxProcessingInstructionCount and if so, it breaks out of the reader, takes the max amount to be processed and re-saves the remaining back to the same record so that this single request does not over-process instructions (which can take a long time and cause all sorts of problems) --- .../Sync/DatabaseServerMessenger.cs | 168 +++++++++++++----- 1 file changed, 125 insertions(+), 43 deletions(-) diff --git a/src/Umbraco.Core/Sync/DatabaseServerMessenger.cs b/src/Umbraco.Core/Sync/DatabaseServerMessenger.cs index 4e46c0ab5c..e8d5287edc 100644 --- a/src/Umbraco.Core/Sync/DatabaseServerMessenger.cs +++ b/src/Umbraco.Core/Sync/DatabaseServerMessenger.cs @@ -115,7 +115,11 @@ namespace Umbraco.Core.Sync { _released = true; // no more syncs } - _syncIdle.WaitOne(); // wait for pending sync + + // Wait for pending sync this is waiting for _syncIdle.Set() + // to be called. Until that is called, the appdomain cannot shut down! + // so whatever is locking this currently should hurry up! + _syncIdle.WaitOne(); }, weight); @@ -197,13 +201,15 @@ namespace Umbraco.Core.Sync { if (_syncing) return; - + + //Don't continue if we are released if (_released) return; if ((DateTime.UtcNow - _lastSync).TotalSeconds <= _options.ThrottleSeconds) return; + //Set our flag and the lock to be in it's original state (i.e. it can be awaited) _syncing = true; _syncIdle.Reset(); _lastSync = DateTime.UtcNow; @@ -215,6 +221,7 @@ namespace Umbraco.Core.Sync { ProcessDatabaseInstructions(); + //Check for pruning throttling if ((DateTime.UtcNow - _lastPruned).TotalSeconds <= _options.PruneThrottleSeconds) return; @@ -231,6 +238,7 @@ namespace Umbraco.Core.Sync } finally { + //We must reset our flag and signal any waiting locks _syncing = false; _syncIdle.Set(); } @@ -255,13 +263,14 @@ namespace Umbraco.Core.Sync // // FIXME not true if we're running on a background thread, assuming we can? + var sql = new Sql().Select("*") .From(_appContext.DatabaseContext.SqlSyntax) .Where(dto => dto.Id > _lastId) .OrderBy(dto => dto.Id, _appContext.DatabaseContext.SqlSyntax); - var dtos = _appContext.DatabaseContext.Database.Fetch(sql); - if (dtos.Count <= 0) return; + //only retrieve the max (just in case there's tons) + var topSql = _appContext.DatabaseContext.SqlSyntax.SelectTop(sql, _options.MaxProcessingInstructionCount); // only process instructions coming from a remote server, and ignore instructions coming from // the local server as they've already been processed. We should NOT assume that the sequence of @@ -269,7 +278,16 @@ namespace Umbraco.Core.Sync var localIdentity = LocalIdentity; var lastId = 0; - foreach (var dto in dtos) + + //this is used to determine if we need to exit the reader loop below because there are actually + // too many instructions to process. In which case we need to exit the reader so we can actually re-save + // the remaining instructions back to the same row (we cannot save something while inside a reader loop) + Tuple> maxInstructions = null; + + //IMPORTANT! We are doing a Query here instead of a Fetch, this means that it will open a data reader + // which we are iterating over instead of loading everything into memory and iterating over that. + // When doing this we always must use a for loop so that the Enumerator is disposed and the reader is closed. + foreach (var dto in _appContext.DatabaseContext.Database.Query(topSql)) { if (dto.OriginIdentity == localIdentity) { @@ -291,27 +309,73 @@ namespace Umbraco.Core.Sync continue; } - // execute remote instructions & update lastId - try - { - NotifyRefreshers(jsonA); - lastId = dto.Id; - } - catch (Exception ex) - { - _logger.Error( - string.Format("DISTRIBUTED CACHE IS NOT UPDATED. Failed to execute instructions ({0}: \"{1}\"). Instruction is being skipped/ignored", dto.Id, dto.Instructions), ex); + var instructionBatch = GetAllInstructions(jsonA); - //we cannot throw here because this invalid instruction will just keep getting processed over and over and errors - // will be thrown over and over. The only thing we can do is ignore and move on. - lastId = dto.Id; + // Here we should check if there's too many instructions, if there is we should split them and re-save the instructions entry with + // the trimmed instructions. We then don't update the lastsynced value so that this row is re-processed again but with only the remaining + // instructions in it. + if (instructionBatch.Count > _options.MaxProcessingInstructionCount) + { + maxInstructions = new Tuple>(dto, instructionBatch); + break; } + + //process as per-normal + lastId = ProcessDatabaseInstructions(instructionBatch, dto); + } + + //If this is not null this means we've found a row that has a ton of instructions in it and we'll need to process + // just a part of it and then re-save the remaining to the same row so that another request can deal with the data. + if (maxInstructions != null) + { + var remainingCount = maxInstructions.Item2.Count - _options.MaxProcessingInstructionCount; + + _logger.Info( + "Max processing instruction count reached. This batch will be processed now but the remaining {0} will be processed by subsequent requests.", () => remainingCount); + + var processingBatch = maxInstructions.Item2.GetRange(0, _options.MaxProcessingInstructionCount); + //NOTE: We are not persisting the lastId from the result of this method because we will need to re-process it + ProcessDatabaseInstructions(processingBatch, maxInstructions.Item1); + + //Save the instruction blob back to the DB with the trimmed instruction count + var remaining = maxInstructions.Item2.GetRange(_options.MaxProcessingInstructionCount - 1, remainingCount); + maxInstructions.Item1.UtcStamp = DateTime.UtcNow; + //serialize the remaining instructions (leave the original identity as-is) + maxInstructions.Item1.Instructions = JsonConvert.SerializeObject(remaining, Formatting.None); + ApplicationContext.DatabaseContext.Database.Update(maxInstructions.Item1); } if (lastId > 0) SaveLastSynced(lastId); } + private int ProcessDatabaseInstructions(List instructionBatch, CacheInstructionDto dto) + { + // execute remote instructions & update lastId + try + { + NotifyRefreshers(instructionBatch); + return dto.Id; + } + //catch (ThreadAbortException ex) + //{ + // //This will occur if the instructions processing is taking too long since this is occuring on a request thread. + // // Or possibly if IIS terminates the appdomain. In any case, we should deal with this differently perhaps... + //} + catch (Exception ex) + { + _logger.Error( + string.Format("DISTRIBUTED CACHE IS NOT UPDATED. Failed to execute instructions (id: {0}, instruction count: {1}). Instruction is being skipped/ignored", dto.Id, instructionBatch.Count), ex); + + //we cannot throw here because this invalid instruction will just keep getting processed over and over and errors + // will be thrown over and over. The only thing we can do is ignore and move on. + return dto.Id; + } + + ////if this is returned it will not be saved + //return -1; + } + /// /// Remove old instructions from the database /// @@ -459,8 +523,14 @@ namespace Umbraco.Core.Sync return jsonRefresher; } - private static void NotifyRefreshers(IEnumerable jsonArray) + /// + /// Parses out the individual instructions to be processed + /// + /// + /// + private static List GetAllInstructions(IEnumerable jsonArray) { + var result = new List(); foreach (var jsonItem in jsonArray) { // could be a JObject in which case we can convert to a RefreshInstruction, @@ -469,33 +539,45 @@ namespace Umbraco.Core.Sync if (jsonObj != null) { var instruction = jsonObj.ToObject(); - switch (instruction.RefreshType) - { - case RefreshMethodType.RefreshAll: - RefreshAll(instruction.RefresherId); - break; - case RefreshMethodType.RefreshByGuid: - RefreshByGuid(instruction.RefresherId, instruction.GuidId); - break; - case RefreshMethodType.RefreshById: - RefreshById(instruction.RefresherId, instruction.IntId); - break; - case RefreshMethodType.RefreshByIds: - RefreshByIds(instruction.RefresherId, instruction.JsonIds); - break; - case RefreshMethodType.RefreshByJson: - RefreshByJson(instruction.RefresherId, instruction.JsonPayload); - break; - case RefreshMethodType.RemoveById: - RemoveById(instruction.RefresherId, instruction.IntId); - break; - } - + result.Add(instruction); } else { - var jsonInnerArray = (JArray) jsonItem; - NotifyRefreshers(jsonInnerArray); // recurse + var jsonInnerArray = (JArray)jsonItem; + result.AddRange(GetAllInstructions(jsonInnerArray)); // recurse + } + } + return result; + } + + /// + /// executes the instructions against the cache refresher instances + /// + /// + private static void NotifyRefreshers(IEnumerable instructions) + { + foreach (var instruction in instructions) + { + switch (instruction.RefreshType) + { + case RefreshMethodType.RefreshAll: + RefreshAll(instruction.RefresherId); + break; + case RefreshMethodType.RefreshByGuid: + RefreshByGuid(instruction.RefresherId, instruction.GuidId); + break; + case RefreshMethodType.RefreshById: + RefreshById(instruction.RefresherId, instruction.IntId); + break; + case RefreshMethodType.RefreshByIds: + RefreshByIds(instruction.RefresherId, instruction.JsonIds); + break; + case RefreshMethodType.RefreshByJson: + RefreshByJson(instruction.RefresherId, instruction.JsonPayload); + break; + case RefreshMethodType.RemoveById: + RemoveById(instruction.RefresherId, instruction.IntId); + break; } } } From 68e871d175e3009ffb71a756386514ba01e3ef2f Mon Sep 17 00:00:00 2001 From: Shannon Date: Tue, 25 Oct 2016 16:01:55 +0200 Subject: [PATCH 27/48] Ensures that any given JSON blob written to the instructions can only contain the maximum number of instructions to process --- .../Sync/DatabaseServerMessenger.cs | 30 +++++++++---------- .../BatchedDatabaseServerMessenger.cs | 22 +++++++++++--- 2 files changed, 33 insertions(+), 19 deletions(-) diff --git a/src/Umbraco.Core/Sync/DatabaseServerMessenger.cs b/src/Umbraco.Core/Sync/DatabaseServerMessenger.cs index e8d5287edc..171635b8ab 100644 --- a/src/Umbraco.Core/Sync/DatabaseServerMessenger.cs +++ b/src/Umbraco.Core/Sync/DatabaseServerMessenger.cs @@ -28,8 +28,7 @@ namespace Umbraco.Core.Sync // public class DatabaseServerMessenger : ServerMessengerBase { - private readonly ApplicationContext _appContext; - private readonly DatabaseServerMessengerOptions _options; + private readonly ApplicationContext _appContext; private readonly ManualResetEvent _syncIdle; private readonly object _locko = new object(); private readonly ILogger _logger; @@ -41,6 +40,7 @@ namespace Umbraco.Core.Sync private bool _released; private readonly ProfilingLogger _profilingLogger; + protected DatabaseServerMessengerOptions Options { get; private set; } protected ApplicationContext ApplicationContext { get { return _appContext; } } public DatabaseServerMessenger(ApplicationContext appContext, bool distributedEnabled, DatabaseServerMessengerOptions options) @@ -50,7 +50,7 @@ namespace Umbraco.Core.Sync if (options == null) throw new ArgumentNullException("options"); _appContext = appContext; - _options = options; + Options = options; _lastPruned = _lastSync = DateTime.UtcNow; _syncIdle = new ManualResetEvent(true); _profilingLogger = appContext.ProfilingLogger; @@ -159,13 +159,13 @@ namespace Umbraco.Core.Sync { //check for how many instructions there are to process var count = _appContext.DatabaseContext.Database.ExecuteScalar("SELECT COUNT(*) FROM umbracoCacheInstruction WHERE id > @lastId", new {lastId = _lastId}); - if (count > _options.MaxProcessingInstructionCount) + if (count > Options.MaxProcessingInstructionCount) { //too many instructions, proceed to cold boot _logger.Warn("The instruction count ({0}) exceeds the specified MaxProcessingInstructionCount ({1})." + " The server will skip existing instructions, rebuild its caches and indexes entirely, adjust its last synced Id" + " to the latest found in the database and maintain cache updates based on that Id.", - () => count, () => _options.MaxProcessingInstructionCount); + () => count, () => Options.MaxProcessingInstructionCount); coldboot = true; } @@ -183,8 +183,8 @@ namespace Umbraco.Core.Sync SaveLastSynced(maxId); // execute initializing callbacks - if (_options.InitializingCallbacks != null) - foreach (var callback in _options.InitializingCallbacks) + if (Options.InitializingCallbacks != null) + foreach (var callback in Options.InitializingCallbacks) callback(); } @@ -206,7 +206,7 @@ namespace Umbraco.Core.Sync if (_released) return; - if ((DateTime.UtcNow - _lastSync).TotalSeconds <= _options.ThrottleSeconds) + if ((DateTime.UtcNow - _lastSync).TotalSeconds <= Options.ThrottleSeconds) return; //Set our flag and the lock to be in it's original state (i.e. it can be awaited) @@ -222,7 +222,7 @@ namespace Umbraco.Core.Sync ProcessDatabaseInstructions(); //Check for pruning throttling - if ((DateTime.UtcNow - _lastPruned).TotalSeconds <= _options.PruneThrottleSeconds) + if ((DateTime.UtcNow - _lastPruned).TotalSeconds <= Options.PruneThrottleSeconds) return; _lastPruned = _lastSync; @@ -270,7 +270,7 @@ namespace Umbraco.Core.Sync .OrderBy(dto => dto.Id, _appContext.DatabaseContext.SqlSyntax); //only retrieve the max (just in case there's tons) - var topSql = _appContext.DatabaseContext.SqlSyntax.SelectTop(sql, _options.MaxProcessingInstructionCount); + var topSql = _appContext.DatabaseContext.SqlSyntax.SelectTop(sql, Options.MaxProcessingInstructionCount); // only process instructions coming from a remote server, and ignore instructions coming from // the local server as they've already been processed. We should NOT assume that the sequence of @@ -314,7 +314,7 @@ namespace Umbraco.Core.Sync // Here we should check if there's too many instructions, if there is we should split them and re-save the instructions entry with // the trimmed instructions. We then don't update the lastsynced value so that this row is re-processed again but with only the remaining // instructions in it. - if (instructionBatch.Count > _options.MaxProcessingInstructionCount) + if (instructionBatch.Count > Options.MaxProcessingInstructionCount) { maxInstructions = new Tuple>(dto, instructionBatch); break; @@ -328,17 +328,17 @@ namespace Umbraco.Core.Sync // just a part of it and then re-save the remaining to the same row so that another request can deal with the data. if (maxInstructions != null) { - var remainingCount = maxInstructions.Item2.Count - _options.MaxProcessingInstructionCount; + var remainingCount = maxInstructions.Item2.Count - Options.MaxProcessingInstructionCount; _logger.Info( "Max processing instruction count reached. This batch will be processed now but the remaining {0} will be processed by subsequent requests.", () => remainingCount); - var processingBatch = maxInstructions.Item2.GetRange(0, _options.MaxProcessingInstructionCount); + var processingBatch = maxInstructions.Item2.GetRange(0, Options.MaxProcessingInstructionCount); //NOTE: We are not persisting the lastId from the result of this method because we will need to re-process it ProcessDatabaseInstructions(processingBatch, maxInstructions.Item1); //Save the instruction blob back to the DB with the trimmed instruction count - var remaining = maxInstructions.Item2.GetRange(_options.MaxProcessingInstructionCount - 1, remainingCount); + var remaining = maxInstructions.Item2.GetRange(Options.MaxProcessingInstructionCount - 1, remainingCount); maxInstructions.Item1.UtcStamp = DateTime.UtcNow; //serialize the remaining instructions (leave the original identity as-is) maxInstructions.Item1.Instructions = JsonConvert.SerializeObject(remaining, Formatting.None); @@ -386,7 +386,7 @@ namespace Umbraco.Core.Sync /// private void PruneOldInstructions() { - var pruneDate = DateTime.UtcNow.AddDays(-_options.DaysToRetainInstructions); + var pruneDate = DateTime.UtcNow.AddDays(-Options.DaysToRetainInstructions); // using 2 queries is faster than convoluted joins diff --git a/src/Umbraco.Web/BatchedDatabaseServerMessenger.cs b/src/Umbraco.Web/BatchedDatabaseServerMessenger.cs index 730efd380b..f06a7a332f 100644 --- a/src/Umbraco.Web/BatchedDatabaseServerMessenger.cs +++ b/src/Umbraco.Web/BatchedDatabaseServerMessenger.cs @@ -84,11 +84,16 @@ namespace Umbraco.Web var instructions = batch.SelectMany(x => x.Instructions).ToArray(); batch.Clear(); - if (instructions.Length == 0) return; - WriteInstructions(instructions); + + //Write the instructions but only create JSON blobs with a max instruction count equal to MaxProcessingInstructionCount + foreach (var instructionsBatch in instructions.InGroupsOf(Options.MaxProcessingInstructionCount)) + { + WriteInstructions(instructionsBatch); + } + } - private void WriteInstructions(RefreshInstruction[] instructions) + private void WriteInstructions(IEnumerable instructions) { var dto = new CacheInstructionDto { @@ -136,9 +141,18 @@ namespace Umbraco.Web // batch if we can, else write to DB immediately if (batch == null) - WriteInstructions(instructions.ToArray()); + { + //only write the json blob with a maximum count of the MaxProcessingInstructionCount + foreach (var maxBatch in instructions.InGroupsOf(Options.MaxProcessingInstructionCount)) + { + WriteInstructions(maxBatch); + } + } else + { batch.Add(new RefreshInstructionEnvelope(servers, refresher, instructions)); + } + } } } \ No newline at end of file From 876d78b4adf1d7587ece149e50879c461764ad6f Mon Sep 17 00:00:00 2001 From: Shannon Date: Tue, 25 Oct 2016 16:23:49 +0200 Subject: [PATCH 28/48] During instruction processing, always check if the app is shutting down and if so then exit, if it's in the middle of a batch it will exit even if it's processed a few and will not save the last synced id meaning the batch will get processed again .Reverts the change to process only a portion of the instructions json blob and save back to the db - that won't work in LB scenarios. --- .../Sync/DatabaseServerMessenger.cs | 83 ++++++++++--------- 1 file changed, 42 insertions(+), 41 deletions(-) diff --git a/src/Umbraco.Core/Sync/DatabaseServerMessenger.cs b/src/Umbraco.Core/Sync/DatabaseServerMessenger.cs index 171635b8ab..b6dda18cb7 100644 --- a/src/Umbraco.Core/Sync/DatabaseServerMessenger.cs +++ b/src/Umbraco.Core/Sync/DatabaseServerMessenger.cs @@ -278,17 +278,19 @@ namespace Umbraco.Core.Sync var localIdentity = LocalIdentity; var lastId = 0; - - //this is used to determine if we need to exit the reader loop below because there are actually - // too many instructions to process. In which case we need to exit the reader so we can actually re-save - // the remaining instructions back to the same row (we cannot save something while inside a reader loop) - Tuple> maxInstructions = null; - + //IMPORTANT! We are doing a Query here instead of a Fetch, this means that it will open a data reader // which we are iterating over instead of loading everything into memory and iterating over that. // When doing this we always must use a for loop so that the Enumerator is disposed and the reader is closed. foreach (var dto in _appContext.DatabaseContext.Database.Query(topSql)) { + //If this flag gets set it means we're shutting down! In this case, we need to exit asap and cannot + // continue processing anything otherwise we'll hold up the app domain shutdown + if (_released) + { + break; + } + if (dto.OriginIdentity == localIdentity) { // just skip that local one but update lastId nevertheless @@ -311,51 +313,39 @@ namespace Umbraco.Core.Sync var instructionBatch = GetAllInstructions(jsonA); - // Here we should check if there's too many instructions, if there is we should split them and re-save the instructions entry with - // the trimmed instructions. We then don't update the lastsynced value so that this row is re-processed again but with only the remaining - // instructions in it. - if (instructionBatch.Count > Options.MaxProcessingInstructionCount) - { - maxInstructions = new Tuple>(dto, instructionBatch); - break; - } - //process as per-normal - lastId = ProcessDatabaseInstructions(instructionBatch, dto); - } + var success = ProcessDatabaseInstructions(instructionBatch, dto, ref lastId); - //If this is not null this means we've found a row that has a ton of instructions in it and we'll need to process - // just a part of it and then re-save the remaining to the same row so that another request can deal with the data. - if (maxInstructions != null) - { - var remainingCount = maxInstructions.Item2.Count - Options.MaxProcessingInstructionCount; - - _logger.Info( - "Max processing instruction count reached. This batch will be processed now but the remaining {0} will be processed by subsequent requests.", () => remainingCount); - - var processingBatch = maxInstructions.Item2.GetRange(0, Options.MaxProcessingInstructionCount); - //NOTE: We are not persisting the lastId from the result of this method because we will need to re-process it - ProcessDatabaseInstructions(processingBatch, maxInstructions.Item1); - - //Save the instruction blob back to the DB with the trimmed instruction count - var remaining = maxInstructions.Item2.GetRange(Options.MaxProcessingInstructionCount - 1, remainingCount); - maxInstructions.Item1.UtcStamp = DateTime.UtcNow; - //serialize the remaining instructions (leave the original identity as-is) - maxInstructions.Item1.Instructions = JsonConvert.SerializeObject(remaining, Formatting.None); - ApplicationContext.DatabaseContext.Database.Update(maxInstructions.Item1); + //if they couldn't be all processed (i.e. we're shutting down) then exit + if (success == false) + break; } if (lastId > 0) SaveLastSynced(lastId); } - private int ProcessDatabaseInstructions(List instructionBatch, CacheInstructionDto dto) + /// + /// Processes the instruction batch and checks for errors + /// + /// + /// + /// + /// + /// returns true if all instructions in the batch were processed, otherwise false if they could not be due to the app being shut down + /// + private bool ProcessDatabaseInstructions(IReadOnlyCollection instructionBatch, CacheInstructionDto dto, ref int lastId) { // execute remote instructions & update lastId try { - NotifyRefreshers(instructionBatch); - return dto.Id; + var result = NotifyRefreshers(instructionBatch); + if (result) + { + //if all instructions we're processed, set the last id + lastId = dto.Id; + } + return result; } //catch (ThreadAbortException ex) //{ @@ -369,7 +359,8 @@ namespace Umbraco.Core.Sync //we cannot throw here because this invalid instruction will just keep getting processed over and over and errors // will be thrown over and over. The only thing we can do is ignore and move on. - return dto.Id; + lastId = dto.Id; + return false; } ////if this is returned it will not be saved @@ -554,10 +545,19 @@ namespace Umbraco.Core.Sync /// executes the instructions against the cache refresher instances /// /// - private static void NotifyRefreshers(IEnumerable instructions) + /// + /// Returns true if all instructions were processed, otherwise false if the processing was interupted (i.e. app shutdown) + /// + private bool NotifyRefreshers(IEnumerable instructions) { foreach (var instruction in instructions) { + //Check if the app is shutting down, we need to exit if this happens. + if (_released) + { + return false; + } + switch (instruction.RefreshType) { case RefreshMethodType.RefreshAll: @@ -580,6 +580,7 @@ namespace Umbraco.Core.Sync break; } } + return true; } private static void RefreshAll(Guid uniqueIdentifier) From f153332ad5e49425b18b07b4431e4486e6663391 Mon Sep 17 00:00:00 2001 From: Shannon Date: Tue, 25 Oct 2016 16:56:55 +0200 Subject: [PATCH 29/48] updates logging levels for MainDom --- src/Umbraco.Core/MainDom.cs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/Umbraco.Core/MainDom.cs b/src/Umbraco.Core/MainDom.cs index 6f4a539194..49d4b9b36a 100644 --- a/src/Umbraco.Core/MainDom.cs +++ b/src/Umbraco.Core/MainDom.cs @@ -118,7 +118,7 @@ namespace Umbraco.Core // in any case... _isMainDom = false; _asyncLocker.Dispose(); - _logger.Debug("Released MainDom."); + _logger.Info("Released MainDom."); } } @@ -131,11 +131,11 @@ namespace Umbraco.Core // the handler is not installed so that would be the hosting environment if (_signaled) { - _logger.Debug("Cannot acquire MainDom (signaled)."); + _logger.Info("Cannot acquire MainDom (signaled)."); return false; } - _logger.Debug("Acquiring MainDom..."); + _logger.Info("Acquiring MainDom..."); // signal other instances that we want the lock, then wait one the lock, // which may timeout, and this is accepted - see comments below @@ -162,7 +162,7 @@ namespace Umbraco.Core HostingEnvironment.RegisterObject(this); - _logger.Debug("Acquired MainDom."); + _logger.Info("Acquired MainDom."); return true; } } From db7c20d99a932943d5b058aff5478b015769a73c Mon Sep 17 00:00:00 2001 From: Claus Date: Wed, 26 Oct 2016 11:32:25 +0200 Subject: [PATCH 30/48] fixing unit test failing on da-DK culture sorting. --- .../Persistence/Repositories/ContentTypeRepositoryTest.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Umbraco.Tests/Persistence/Repositories/ContentTypeRepositoryTest.cs b/src/Umbraco.Tests/Persistence/Repositories/ContentTypeRepositoryTest.cs index 7d8d7659c8..84cdef73e1 100644 --- a/src/Umbraco.Tests/Persistence/Repositories/ContentTypeRepositoryTest.cs +++ b/src/Umbraco.Tests/Persistence/Repositories/ContentTypeRepositoryTest.cs @@ -539,7 +539,7 @@ namespace Umbraco.Tests.Persistence.Repositories using (var repository = CreateRepository(unitOfWork)) { var contentType = repository.Get(NodeDto.NodeIdSeed + 1); - var child1 = MockedContentTypes.CreateSimpleContentType("aabc", "aabc", contentType, randomizeAliases: true); + var child1 = MockedContentTypes.CreateSimpleContentType("abc", "abc", contentType, randomizeAliases: true); repository.AddOrUpdate(child1); var child3 = MockedContentTypes.CreateSimpleContentType("zyx", "zyx", contentType, randomizeAliases: true); repository.AddOrUpdate(child3); @@ -553,7 +553,7 @@ namespace Umbraco.Tests.Persistence.Repositories // Assert Assert.That(contentTypes.Count(), Is.EqualTo(3)); Assert.AreEqual("a123", contentTypes.ElementAt(0).Name); - Assert.AreEqual("aabc", contentTypes.ElementAt(1).Name); + Assert.AreEqual("abc", contentTypes.ElementAt(1).Name); Assert.AreEqual("zyx", contentTypes.ElementAt(2).Name); } From 8b411c3d4662140b41e2aa099fd27780ffd01180 Mon Sep 17 00:00:00 2001 From: Shannon Date: Wed, 26 Oct 2016 11:37:59 +0200 Subject: [PATCH 31/48] Adds checks to see if the app is shutting down while processing instruction batches, if so it will exit instruction processing. Also set a timeout on the wait lock so that if the app is shutting down it will after 5 seconds. --- .../Sync/DatabaseServerMessenger.cs | 41 ++++++++++++++----- 1 file changed, 30 insertions(+), 11 deletions(-) diff --git a/src/Umbraco.Core/Sync/DatabaseServerMessenger.cs b/src/Umbraco.Core/Sync/DatabaseServerMessenger.cs index b6dda18cb7..ccd0a7b71d 100644 --- a/src/Umbraco.Core/Sync/DatabaseServerMessenger.cs +++ b/src/Umbraco.Core/Sync/DatabaseServerMessenger.cs @@ -116,10 +116,13 @@ namespace Umbraco.Core.Sync _released = true; // no more syncs } - // Wait for pending sync this is waiting for _syncIdle.Set() - // to be called. Until that is called, the appdomain cannot shut down! - // so whatever is locking this currently should hurry up! - _syncIdle.WaitOne(); + //only wait 5 seconds + var result =_syncIdle.WaitOne(5000); + if (result == false) + { + //a timeout occurred :/ + _logger.Warn("The wait lock timed out, application is shutting down. The current instruction batch will be re-processed."); + } }, weight); @@ -222,7 +225,7 @@ namespace Umbraco.Core.Sync ProcessDatabaseInstructions(); //Check for pruning throttling - if ((DateTime.UtcNow - _lastPruned).TotalSeconds <= Options.PruneThrottleSeconds) + if ((_released || (DateTime.UtcNow - _lastPruned).TotalSeconds <= Options.PruneThrottleSeconds)) return; _lastPruned = _lastSync; @@ -238,8 +241,12 @@ namespace Umbraco.Core.Sync } finally { - //We must reset our flag and signal any waiting locks - _syncing = false; + lock (_locko) + { + //We must reset our flag and signal any waiting locks + _syncing = false; + } + _syncIdle.Set(); } } @@ -279,10 +286,10 @@ namespace Umbraco.Core.Sync var lastId = 0; - //IMPORTANT! We are doing a Query here instead of a Fetch, this means that it will open a data reader - // which we are iterating over instead of loading everything into memory and iterating over that. - // When doing this we always must use a for loop so that the Enumerator is disposed and the reader is closed. - foreach (var dto in _appContext.DatabaseContext.Database.Query(topSql)) + //It would have been nice to do this in a Query instead of Fetch using a data reader to save + // some memory however we cannot do thta because inside of this loop the cache refreshers are also + // performing some lookups which cannot be done with an active reader open + foreach (var dto in _appContext.DatabaseContext.Database.Fetch(topSql)) { //If this flag gets set it means we're shutting down! In this case, we need to exit asap and cannot // continue processing anything otherwise we'll hold up the app domain shutdown @@ -318,7 +325,11 @@ namespace Umbraco.Core.Sync //if they couldn't be all processed (i.e. we're shutting down) then exit if (success == false) + { + _logger.Info("The current batch of instructions was not processed, app is shutting down"); break; + } + } if (lastId > 0) @@ -550,6 +561,8 @@ namespace Umbraco.Core.Sync /// private bool NotifyRefreshers(IEnumerable instructions) { + var processed = new HashSet(); + foreach (var instruction in instructions) { //Check if the app is shutting down, we need to exit if this happens. @@ -558,6 +571,10 @@ namespace Umbraco.Core.Sync return false; } + //this has already been processed + if (processed.Contains(instruction)) + continue; + switch (instruction.RefreshType) { case RefreshMethodType.RefreshAll: @@ -579,6 +596,8 @@ namespace Umbraco.Core.Sync RemoveById(instruction.RefresherId, instruction.IntId); break; } + + processed.Add(instruction); } return true; } From c667627d2fe139e28181a09a3a700bde8830bc69 Mon Sep 17 00:00:00 2001 From: Shannon Date: Wed, 26 Oct 2016 12:01:43 +0200 Subject: [PATCH 32/48] Changes max row retrieval to 100 --- src/Umbraco.Core/Sync/DatabaseServerMessenger.cs | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/src/Umbraco.Core/Sync/DatabaseServerMessenger.cs b/src/Umbraco.Core/Sync/DatabaseServerMessenger.cs index ccd0a7b71d..a876503f50 100644 --- a/src/Umbraco.Core/Sync/DatabaseServerMessenger.cs +++ b/src/Umbraco.Core/Sync/DatabaseServerMessenger.cs @@ -161,6 +161,8 @@ namespace Umbraco.Core.Sync else { //check for how many instructions there are to process + //TODO: In 7.6 we need to store the count of instructions per row since this is not affective because there can be far more than one (if not thousands) + // of instructions in a single row. var count = _appContext.DatabaseContext.Database.ExecuteScalar("SELECT COUNT(*) FROM umbracoCacheInstruction WHERE id > @lastId", new {lastId = _lastId}); if (count > Options.MaxProcessingInstructionCount) { @@ -276,8 +278,11 @@ namespace Umbraco.Core.Sync .Where(dto => dto.Id > _lastId) .OrderBy(dto => dto.Id, _appContext.DatabaseContext.SqlSyntax); - //only retrieve the max (just in case there's tons) - var topSql = _appContext.DatabaseContext.SqlSyntax.SelectTop(sql, Options.MaxProcessingInstructionCount); + //only retrieve the top 100 (just in case there's tons) + // even though MaxProcessingInstructionCount is by default 1000 we still don't want to process that many + // rows in one request thread since each row can contain a ton of instructions (until 7.5.5 in which case + // a row can only contain MaxProcessingInstructionCount) + var topSql = _appContext.DatabaseContext.SqlSyntax.SelectTop(sql, 100); // only process instructions coming from a remote server, and ignore instructions coming from // the local server as they've already been processed. We should NOT assume that the sequence of From 8e4e7e5b19fea7f121ac7a93d52becf7286691da Mon Sep 17 00:00:00 2001 From: Shannon Date: Wed, 26 Oct 2016 12:19:36 +0200 Subject: [PATCH 33/48] Ensures no duplicates are processed between rows --- .../Sync/DatabaseServerMessenger.cs | 19 ++++++++++++------- 1 file changed, 12 insertions(+), 7 deletions(-) diff --git a/src/Umbraco.Core/Sync/DatabaseServerMessenger.cs b/src/Umbraco.Core/Sync/DatabaseServerMessenger.cs index a876503f50..d899e41779 100644 --- a/src/Umbraco.Core/Sync/DatabaseServerMessenger.cs +++ b/src/Umbraco.Core/Sync/DatabaseServerMessenger.cs @@ -116,7 +116,7 @@ namespace Umbraco.Core.Sync _released = true; // no more syncs } - //only wait 5 seconds + //wait a max of 5 seconds var result =_syncIdle.WaitOne(5000); if (result == false) { @@ -291,6 +291,9 @@ namespace Umbraco.Core.Sync var lastId = 0; + //tracks which ones have already been processed to avoid duplicates + var processed = new HashSet(); + //It would have been nice to do this in a Query instead of Fetch using a data reader to save // some memory however we cannot do thta because inside of this loop the cache refreshers are also // performing some lookups which cannot be done with an active reader open @@ -326,7 +329,7 @@ namespace Umbraco.Core.Sync var instructionBatch = GetAllInstructions(jsonA); //process as per-normal - var success = ProcessDatabaseInstructions(instructionBatch, dto, ref lastId); + var success = ProcessDatabaseInstructions(instructionBatch, dto, processed, ref lastId); //if they couldn't be all processed (i.e. we're shutting down) then exit if (success == false) @@ -346,16 +349,19 @@ namespace Umbraco.Core.Sync /// /// /// + /// + /// Tracks which instructions have already been processed to avoid duplicates + /// /// /// /// returns true if all instructions in the batch were processed, otherwise false if they could not be due to the app being shut down /// - private bool ProcessDatabaseInstructions(IReadOnlyCollection instructionBatch, CacheInstructionDto dto, ref int lastId) + private bool ProcessDatabaseInstructions(IReadOnlyCollection instructionBatch, CacheInstructionDto dto, HashSet processed, ref int lastId) { // execute remote instructions & update lastId try { - var result = NotifyRefreshers(instructionBatch); + var result = NotifyRefreshers(instructionBatch, processed); if (result) { //if all instructions we're processed, set the last id @@ -561,13 +567,12 @@ namespace Umbraco.Core.Sync /// executes the instructions against the cache refresher instances /// /// + /// /// /// Returns true if all instructions were processed, otherwise false if the processing was interupted (i.e. app shutdown) /// - private bool NotifyRefreshers(IEnumerable instructions) + private bool NotifyRefreshers(IEnumerable instructions, HashSet processed) { - var processed = new HashSet(); - foreach (var instruction in instructions) { //Check if the app is shutting down, we need to exit if this happens. From 6d0bca471e26cdbcf361f2443dd9f3837d6a1f01 Mon Sep 17 00:00:00 2001 From: Stephan Date: Wed, 26 Oct 2016 14:24:31 +0200 Subject: [PATCH 34/48] U4-8995 - fix health check for trySkipIisError --- .../HealthCheck/Checks/Config/TrySkipIisCustomErrorsCheck.cs | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/Umbraco.Web/HealthCheck/Checks/Config/TrySkipIisCustomErrorsCheck.cs b/src/Umbraco.Web/HealthCheck/Checks/Config/TrySkipIisCustomErrorsCheck.cs index 654d7dd209..2b9f1aa2a2 100644 --- a/src/Umbraco.Web/HealthCheck/Checks/Config/TrySkipIisCustomErrorsCheck.cs +++ b/src/Umbraco.Web/HealthCheck/Checks/Config/TrySkipIisCustomErrorsCheck.cs @@ -38,7 +38,8 @@ namespace Umbraco.Web.HealthCheck.Checks.Config { get { - var recommendedValue = _serverVersion >= new Version("7.5.0") + // beware! 7.5 and 7.5.0 are not the same thing! + var recommendedValue = _serverVersion >= new Version("7.5") ? bool.TrueString.ToLower() : bool.FalseString.ToLower(); return new List { new AcceptableConfiguration { IsRecommended = true, Value = recommendedValue } }; @@ -50,7 +51,7 @@ namespace Umbraco.Web.HealthCheck.Checks.Config get { return _textService.Localize("healthcheck/trySkipIisCustomErrorsCheckSuccessMessage", - new[] { CurrentValue, Values.First(v => v.IsRecommended).Value, _serverVersion.ToString() }); + new[] { Values.First(v => v.IsRecommended).Value, _serverVersion.ToString() }); } } From 7e81768abc3b90ecedaf9e6a9c6e373aa4dbcbd6 Mon Sep 17 00:00:00 2001 From: Shannon Date: Wed, 26 Oct 2016 14:25:32 +0200 Subject: [PATCH 35/48] adds exception checking to ExamineExtensions --- .../Search/LuceneIndexerExtensions.cs | 52 ++++++++++++++++--- 1 file changed, 44 insertions(+), 8 deletions(-) diff --git a/src/Umbraco.Web/Search/LuceneIndexerExtensions.cs b/src/Umbraco.Web/Search/LuceneIndexerExtensions.cs index 3cc7908dfd..34ad71d2f9 100644 --- a/src/Umbraco.Web/Search/LuceneIndexerExtensions.cs +++ b/src/Umbraco.Web/Search/LuceneIndexerExtensions.cs @@ -5,6 +5,8 @@ using Examine.LuceneEngine.Providers; using Examine.Providers; using Lucene.Net.Index; using Lucene.Net.Search; +using Lucene.Net.Store; +using Umbraco.Core.Logging; namespace Umbraco.Web.Search { @@ -21,9 +23,17 @@ namespace Umbraco.Web.Search /// public static int GetIndexDocumentCount(this LuceneIndexer indexer) { - using (var reader = indexer.GetIndexWriter().GetReader()) + try { - return reader.NumDocs(); + using (var reader = indexer.GetIndexWriter().GetReader()) + { + return reader.NumDocs(); + } + } + catch (AlreadyClosedException) + { + LogHelper.Warn(typeof(ExamineExtensions), "Cannot get GetIndexDocumentCount, the writer is already closed"); + return 0; } } @@ -34,9 +44,19 @@ namespace Umbraco.Web.Search /// public static int GetIndexFieldCount(this LuceneIndexer indexer) { - using (var reader = indexer.GetIndexWriter().GetReader()) + //TODO: check for closing! and AlreadyClosedException + + try { - return reader.GetFieldNames(IndexReader.FieldOption.ALL).Count; + using (var reader = indexer.GetIndexWriter().GetReader()) + { + return reader.GetFieldNames(IndexReader.FieldOption.ALL).Count; + } + } + catch (AlreadyClosedException) + { + LogHelper.Warn(typeof(ExamineExtensions), "Cannot get GetIndexFieldCount, the writer is already closed"); + return 0; } } @@ -47,9 +67,17 @@ namespace Umbraco.Web.Search /// public static bool IsIndexOptimized(this LuceneIndexer indexer) { - using (var reader = indexer.GetIndexWriter().GetReader()) + try { - return reader.IsOptimized(); + using (var reader = indexer.GetIndexWriter().GetReader()) + { + return reader.IsOptimized(); + } + } + catch (AlreadyClosedException) + { + LogHelper.Warn(typeof(ExamineExtensions), "Cannot get IsIndexOptimized, the writer is already closed"); + return false; } } @@ -74,9 +102,17 @@ namespace Umbraco.Web.Search /// public static int GetDeletedDocumentsCount(this LuceneIndexer indexer) { - using (var reader = indexer.GetIndexWriter().GetReader()) + try { - return reader.NumDeletedDocs(); + using (var reader = indexer.GetIndexWriter().GetReader()) + { + return reader.NumDeletedDocs(); + } + } + catch (AlreadyClosedException) + { + LogHelper.Warn(typeof(ExamineExtensions), "Cannot get GetDeletedDocumentsCount, the writer is already closed"); + return 0; } } } From 71afe908b47ad3bc91431e2e8406bdbf2f0b322c Mon Sep 17 00:00:00 2001 From: Stephan Date: Wed, 26 Oct 2016 18:01:32 +0200 Subject: [PATCH 36/48] U4-9101 - comments and misc --- src/Umbraco.Core/MainDom.cs | 6 ++--- .../Sync/DatabaseServerMessenger.cs | 27 ++++++++++--------- 2 files changed, 18 insertions(+), 15 deletions(-) diff --git a/src/Umbraco.Core/MainDom.cs b/src/Umbraco.Core/MainDom.cs index 49d4b9b36a..fb8ad06999 100644 --- a/src/Umbraco.Core/MainDom.cs +++ b/src/Umbraco.Core/MainDom.cs @@ -25,7 +25,7 @@ namespace Umbraco.Core private readonly AsyncLock _asyncLock; private IDisposable _asyncLocker; - // event wait handle used to notify current main domain that it should + // event wait handle used to notify current main domain that it should // release the lock because a new domain wants to be the main domain private readonly EventWaitHandle _signal; @@ -97,7 +97,7 @@ namespace Umbraco.Core try { - _logger.Debug("Stopping..."); + _logger.Info("Stopping..."); foreach (var callback in _callbacks.Values) { try @@ -109,7 +109,7 @@ namespace Umbraco.Core _logger.Error("Error while running callback, remaining callbacks will not run.", e); throw; } - + } _logger.Debug("Stopped."); } diff --git a/src/Umbraco.Core/Sync/DatabaseServerMessenger.cs b/src/Umbraco.Core/Sync/DatabaseServerMessenger.cs index d899e41779..12cb465b50 100644 --- a/src/Umbraco.Core/Sync/DatabaseServerMessenger.cs +++ b/src/Umbraco.Core/Sync/DatabaseServerMessenger.cs @@ -28,7 +28,7 @@ namespace Umbraco.Core.Sync // public class DatabaseServerMessenger : ServerMessengerBase { - private readonly ApplicationContext _appContext; + private readonly ApplicationContext _appContext; private readonly ManualResetEvent _syncIdle; private readonly object _locko = new object(); private readonly ILogger _logger; @@ -116,13 +116,16 @@ namespace Umbraco.Core.Sync _released = true; // no more syncs } - //wait a max of 5 seconds - var result =_syncIdle.WaitOne(5000); - if (result == false) + // wait a max of 5 seconds and then return, so that we don't block + // the entire MainDom callbacks chain and prevent the AppDomain from + // properly releasing MainDom - a timeout here means that one refresher + // is taking too much time processing, however when it's done we will + // not update lastId and stop everything + var idle =_syncIdle.WaitOne(5000); + if (idle == false) { - //a timeout occurred :/ _logger.Warn("The wait lock timed out, application is shutting down. The current instruction batch will be re-processed."); - } + } }, weight); @@ -206,7 +209,7 @@ namespace Umbraco.Core.Sync { if (_syncing) return; - + //Don't continue if we are released if (_released) return; @@ -272,14 +275,14 @@ namespace Umbraco.Core.Sync // // FIXME not true if we're running on a background thread, assuming we can? - + var sql = new Sql().Select("*") .From(_appContext.DatabaseContext.SqlSyntax) .Where(dto => dto.Id > _lastId) .OrderBy(dto => dto.Id, _appContext.DatabaseContext.SqlSyntax); //only retrieve the top 100 (just in case there's tons) - // even though MaxProcessingInstructionCount is by default 1000 we still don't want to process that many + // even though MaxProcessingInstructionCount is by default 1000 we still don't want to process that many // rows in one request thread since each row can contain a ton of instructions (until 7.5.5 in which case // a row can only contain MaxProcessingInstructionCount) var topSql = _appContext.DatabaseContext.SqlSyntax.SelectTop(sql, 100); @@ -290,11 +293,11 @@ namespace Umbraco.Core.Sync var localIdentity = LocalIdentity; var lastId = 0; - + //tracks which ones have already been processed to avoid duplicates var processed = new HashSet(); - //It would have been nice to do this in a Query instead of Fetch using a data reader to save + //It would have been nice to do this in a Query instead of Fetch using a data reader to save // some memory however we cannot do thta because inside of this loop the cache refreshers are also // performing some lookups which cannot be done with an active reader open foreach (var dto in _appContext.DatabaseContext.Database.Fetch(topSql)) @@ -337,7 +340,7 @@ namespace Umbraco.Core.Sync _logger.Info("The current batch of instructions was not processed, app is shutting down"); break; } - + } if (lastId > 0) From 7495d89a7911d2fed257f87ee7179cd0ce73e6f0 Mon Sep 17 00:00:00 2001 From: Stephan Date: Wed, 26 Oct 2016 19:26:10 +0200 Subject: [PATCH 37/48] U4-9107 - enable bench on empty db --- src/Umbraco.Tests.Benchmarks/BulkInsertBenchmarks.cs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/Umbraco.Tests.Benchmarks/BulkInsertBenchmarks.cs b/src/Umbraco.Tests.Benchmarks/BulkInsertBenchmarks.cs index 5212a4f524..05b14a528a 100644 --- a/src/Umbraco.Tests.Benchmarks/BulkInsertBenchmarks.cs +++ b/src/Umbraco.Tests.Benchmarks/BulkInsertBenchmarks.cs @@ -72,7 +72,8 @@ namespace Umbraco.Tests.Benchmarks logger); //drop the table - _dbSqlServer.Execute("DROP TABLE [umbracoServer]"); + // note: DROP TABLE IF EXISTS is SQL 2016+ + _dbSqlServer.Execute("IF OBJECT_ID('dbo.umbracoServer', 'U') IS NOT NULL DROP TABLE [umbracoServer]"); //re-create it _dbSqlServer.Execute(@"CREATE TABLE [umbracoServer]( From ba0ef3676d4b6c27046d7ed4906f471070966dca Mon Sep 17 00:00:00 2001 From: Shannon Date: Thu, 27 Oct 2016 18:32:16 +0200 Subject: [PATCH 38/48] U4-9109 changes all usage of automapper ResolveUsing to instantiate the resolver explicitly - this prevents IoC frameworks registered against automapper to not explode if these resolvers have not been added to the container. --- src/Umbraco.Web/Models/Mapping/ContentModelMapper.cs | 10 +++++----- src/Umbraco.Web/Models/Mapping/DataTypeModelMapper.cs | 4 ++-- src/Umbraco.Web/Models/Mapping/MediaModelMapper.cs | 6 +++--- src/Umbraco.Web/Models/Mapping/MemberModelMapper.cs | 10 +++++----- 4 files changed, 15 insertions(+), 15 deletions(-) diff --git a/src/Umbraco.Web/Models/Mapping/ContentModelMapper.cs b/src/Umbraco.Web/Models/Mapping/ContentModelMapper.cs index e2cdd1f7e4..b7a6ac1f4a 100644 --- a/src/Umbraco.Web/Models/Mapping/ContentModelMapper.cs +++ b/src/Umbraco.Web/Models/Mapping/ContentModelMapper.cs @@ -31,10 +31,10 @@ namespace Umbraco.Web.Models.Mapping config.CreateMap() .ForMember( dto => dto.Owner, - expression => expression.ResolveUsing>()) + expression => expression.ResolveUsing(new OwnerResolver())) .ForMember( dto => dto.Updater, - expression => expression.ResolveUsing()) + expression => expression.ResolveUsing(new CreatorResolver())) .ForMember( dto => dto.Icon, expression => expression.MapFrom(content => content.ContentType.Icon)) @@ -81,10 +81,10 @@ namespace Umbraco.Web.Models.Mapping config.CreateMap>() .ForMember( dto => dto.Owner, - expression => expression.ResolveUsing>()) + expression => expression.ResolveUsing(new OwnerResolver())) .ForMember( dto => dto.Updater, - expression => expression.ResolveUsing()) + expression => expression.ResolveUsing(new CreatorResolver())) .ForMember( dto => dto.Icon, expression => expression.MapFrom(content => content.ContentType.Icon)) @@ -103,7 +103,7 @@ namespace Umbraco.Web.Models.Mapping config.CreateMap>() .ForMember( dto => dto.Owner, - expression => expression.ResolveUsing>()) + expression => expression.ResolveUsing(new OwnerResolver())) .ForMember( dto => dto.HasPublishedVersion, expression => expression.MapFrom(content => content.HasPublishedVersion)) diff --git a/src/Umbraco.Web/Models/Mapping/DataTypeModelMapper.cs b/src/Umbraco.Web/Models/Mapping/DataTypeModelMapper.cs index 70ff63ca12..ac2faa82dc 100644 --- a/src/Umbraco.Web/Models/Mapping/DataTypeModelMapper.cs +++ b/src/Umbraco.Web/Models/Mapping/DataTypeModelMapper.cs @@ -61,7 +61,7 @@ namespace Umbraco.Web.Models.Mapping }); config.CreateMap() - .ForMember(display => display.AvailableEditors, expression => expression.ResolveUsing()) + .ForMember(display => display.AvailableEditors, expression => expression.ResolveUsing(new AvailablePropertyEditorsResolver())) .ForMember(display => display.PreValues, expression => expression.ResolveUsing( new PreValueDisplayResolver(lazyDataTypeService))) .ForMember(display => display.SelectedEditor, expression => expression.MapFrom( @@ -98,7 +98,7 @@ namespace Umbraco.Web.Models.Mapping .ForMember(definition => definition.Key, expression => expression.Ignore()) .ForMember(definition => definition.Path, expression => expression.Ignore()) .ForMember(definition => definition.PropertyEditorAlias, expression => expression.MapFrom(save => save.SelectedEditor)) - .ForMember(definition => definition.DatabaseType, expression => expression.ResolveUsing()) + .ForMember(definition => definition.DatabaseType, expression => expression.ResolveUsing(new DatabaseTypeResolver())) .ForMember(x => x.ControlId, expression => expression.Ignore()) .ForMember(x => x.CreatorId, expression => expression.Ignore()) .ForMember(x => x.Level, expression => expression.Ignore()) diff --git a/src/Umbraco.Web/Models/Mapping/MediaModelMapper.cs b/src/Umbraco.Web/Models/Mapping/MediaModelMapper.cs index 44df5e4d52..d13da67e1f 100644 --- a/src/Umbraco.Web/Models/Mapping/MediaModelMapper.cs +++ b/src/Umbraco.Web/Models/Mapping/MediaModelMapper.cs @@ -31,7 +31,7 @@ namespace Umbraco.Web.Models.Mapping config.CreateMap() .ForMember( dto => dto.Owner, - expression => expression.ResolveUsing>()) + expression => expression.ResolveUsing(new OwnerResolver())) .ForMember( dto => dto.Icon, expression => expression.MapFrom(content => content.ContentType.Icon)) @@ -61,7 +61,7 @@ namespace Umbraco.Web.Models.Mapping config.CreateMap>() .ForMember( dto => dto.Owner, - expression => expression.ResolveUsing>()) + expression => expression.ResolveUsing(new OwnerResolver())) .ForMember( dto => dto.Icon, expression => expression.MapFrom(content => content.ContentType.Icon)) @@ -80,7 +80,7 @@ namespace Umbraco.Web.Models.Mapping config.CreateMap>() .ForMember( dto => dto.Owner, - expression => expression.ResolveUsing>()) + expression => expression.ResolveUsing(new OwnerResolver())) .ForMember(x => x.Published, expression => expression.Ignore()) .ForMember(x => x.Updater, expression => expression.Ignore()) .ForMember(x => x.Icon, expression => expression.Ignore()) diff --git a/src/Umbraco.Web/Models/Mapping/MemberModelMapper.cs b/src/Umbraco.Web/Models/Mapping/MemberModelMapper.cs index 1a3f8862ee..050fe3c726 100644 --- a/src/Umbraco.Web/Models/Mapping/MemberModelMapper.cs +++ b/src/Umbraco.Web/Models/Mapping/MemberModelMapper.cs @@ -64,7 +64,7 @@ namespace Umbraco.Web.Models.Mapping config.CreateMap() .ForMember( dto => dto.Owner, - expression => expression.ResolveUsing>()) + expression => expression.ResolveUsing(new OwnerResolver())) .ForMember( dto => dto.Icon, expression => expression.MapFrom(content => content.ContentType.Icon)) @@ -78,7 +78,7 @@ namespace Umbraco.Web.Models.Mapping .ForMember(display => display.Tabs, expression => expression.ResolveUsing(new MemberTabsAndPropertiesResolver(applicationContext.Services.TextService))) .ForMember(display => display.MemberProviderFieldMapping, - expression => expression.ResolveUsing()) + expression => expression.ResolveUsing(new MemberProviderFieldMappingResolver())) .ForMember(display => display.MembershipScenario, expression => expression.ResolveUsing(new MembershipScenarioMappingResolver(new Lazy(() => applicationContext.Services.MemberTypeService)))) .ForMember(display => display.Notifications, expression => expression.Ignore()) @@ -97,7 +97,7 @@ namespace Umbraco.Web.Models.Mapping config.CreateMap() .ForMember( dto => dto.Owner, - expression => expression.ResolveUsing>()) + expression => expression.ResolveUsing(new OwnerResolver())) .ForMember( dto => dto.Icon, expression => expression.MapFrom(content => content.ContentType.Icon)) @@ -152,14 +152,14 @@ namespace Umbraco.Web.Models.Mapping config.CreateMap>() .ForMember( dto => dto.Owner, - expression => expression.ResolveUsing>()) + expression => expression.ResolveUsing(new OwnerResolver())) .ForMember(x => x.Published, expression => expression.Ignore()) .ForMember(x => x.Updater, expression => expression.Ignore()) .ForMember(x => x.Icon, expression => expression.Ignore()) .ForMember(x => x.Alias, expression => expression.Ignore()) .ForMember(member => member.HasPublishedVersion, expression => expression.Ignore()) //do no map the custom member properties (currently anyways, they were never there in 6.x) - .ForMember(dto => dto.Properties, expression => expression.ResolveUsing()); + .ForMember(dto => dto.Properties, expression => expression.ResolveUsing(new MemberDtoPropertiesValueResolver())); } /// From 3bdb3f8c1ea8249e3a02105658ef07c04a448ade Mon Sep 17 00:00:00 2001 From: Sebastiaan Janssen Date: Thu, 27 Oct 2016 22:07:56 +0200 Subject: [PATCH 39/48] U4-9065 Update github readme from Kris's notes #U4-9065 Fixed --- README.md | 32 +++++++++++++++++++++----------- vimeo.png | Bin 0 -> 27438 bytes 2 files changed, 21 insertions(+), 11 deletions(-) create mode 100644 vimeo.png diff --git a/README.md b/README.md index bce027090f..2758ad3edb 100644 --- a/README.md +++ b/README.md @@ -1,8 +1,15 @@ Umbraco CMS =========== -Umbraco is a free open source Content Management System built on the ASP.NET platform. +The friendliest, most flexible and fastest growing ASP.NET CMS used by more than 350,000 websites worldwide: [https://umbraco.com](https://umbraco.com) + +[![ScreenShot](vimeo.png)](https://vimeo.com/172382998/) + +## Umbraco CMS ## +Umbraco is a free open source Content Management System built on the ASP.NET platform. Our mission is to help you deliver delightful digital experiences by making Umbraco friendly, simpler and social. + ## Building Umbraco from source ## + The easiest way to get started is to run `build/build.bat` which will build both the backoffice (also known as "Belle") and the Umbraco core. You can then easily start debugging from Visual Studio, or if you need to debug Belle you can run `grunt vs` in `src\Umbraco.Web.UI.Client`. If you're interested in making changes to Belle without running Visual Studio make sure to read the [Belle ReadMe file](src/Umbraco.Web.UI.Client/README.md). @@ -13,30 +20,33 @@ Note that you can always [download a nightly build](http://nightly.umbraco.org/? [![ScreenShot](http://umbraco.com/images/whatisumbraco.png)](https://umbraco.tv/videos/umbraco-v7/content-editor/basics/introduction/cms-explanation/) -## Umbraco - the simple, flexible and friendly ASP.NET CMS ## +## Umbraco - The Friendly CMS ## -**More than 350,000 sites trust Umbraco** +For the first time on the Microsoft platform, there is a free user and developer friendly CMS that makes it quick and easy to create websites - or a breeze to build complex web applications. Umbraco has award-winning integration capabilities and supports ASP.NET MVC or Web Forms, including User and Custom Controls, out of the box. -For the first time on the Microsoft platform, there is a free user and developer friendly CMS that makes it quick and easy to create websites - or a breeze to build complex web applications. Umbraco has award-winning integration capabilities and supports ASP.NET MVC or Web Forms, including User and Custom Controls, out of the box. It's a developer's dream and your users will love it too. +Umbraco is not only loved by developers, but is a content editors dream. Enjoy intuitive editing tools, media management, responsive views and approval workflows to send your content live. -Used by more than 350,000 active websites including [http://daviscup.com](http://daviscup.com), [http://heinz.com](http://heinz.com), [http://peugeot.com](http://peugeot.com), [http://www.hersheys.com/](http://www.hersheys.com/) and **The Official ASP.NET and IIS.NET website from Microsoft** ([http://asp.net](http://asp.net) / [http://iis.net](http://iis.net)), you can be sure that the technology is proven, stable and scales. +Used by more than 350,000 active websites including Carlsberg, Segway, Amazon and Heinz and **The Official ASP.NET and IIS.NET website from Microsoft** ([https://asp.net](https://asp.net) / [https://iis.net](https://iis.net)), you can be sure that the technology is proven, stable and scales. Backed by the team at Umbraco HQ, and supported by a dedicated community of over 200,000 craftspeople globally, you can trust that Umbraco is a safe choice and is here to stay. -To view more examples, please visit [http://umbraco.com/why-umbraco/#caseStudies](http://umbraco.com/why-umbraco/#caseStudies) +To view more examples, please visit [https://umbraco.com/why-umbraco/#caseStudies](https://umbraco.com/why-umbraco/#caseStudies) + +## Why Open Source? ## +As an Open Source platform, Umbraco is more than just a CMS. We are transparent with our roadmap for future versions, our incremental sprint planning notes are publicly accessible and community contributions and packages are available for all to use. ## Downloading ## -The downloadable Umbraco releases live at [http://our.umbraco.org/download](http://our.umbraco.org/download). +The downloadable Umbraco releases live at [https://our.umbraco.org/download](https://our.umbraco.org/download). ## Forums ## -We have a forum running on [http://our.umbraco.org](http://our.umbraco.org). The discussions group on [Google Groups](https://groups.google.com/forum/#!forum/umbraco-dev) is for discussions on developing the core, and not on Umbraco-implementations or extensions in general. For those topics, please use [http://our.umbraco.org](http://our.umbraco.org). +Peer-to-peer support is available 24/7 at the community forum on [https://our.umbraco.org](https://our.umbraco.org). ## Contribute to Umbraco ## -If you want to contribute back to Umbraco you should check out our [guide to contributing](http://our.umbraco.org/contribute). +Umbraco is contribution focused and community driven. If you want to contribute back to Umbraco please check out our [guide to contributing](https://our.umbraco.org/contribute). ## Found a bug? ## -Another way you can contribute to Umbraco is by providing issue reports. For information on how to submit an issue report refer to our [online guide for reporting issues](http://our.umbraco.org/contribute/report-an-issue-or-request-a-feature). +Another way you can contribute to Umbraco is by providing issue reports. For information on how to submit an issue report refer to our [online guide for reporting issues](https://our.umbraco.org/contribute/report-an-issue-or-request-a-feature). -To view existing issues, please visit [http://issues.umbraco.org](http://issues.umbraco.org). +To view existing issues, please visit [http://issues.umbraco.org](http://issues.umbraco.org). \ No newline at end of file diff --git a/vimeo.png b/vimeo.png new file mode 100644 index 0000000000000000000000000000000000000000..d3c12465bacf5816041ca754a9a6ac4113ffe726 GIT binary patch literal 27438 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 GUj84hS(HKm literal 0 HcmV?d00001 From c500f98ad8137b86e71b03b159089e3344ab8553 Mon Sep 17 00:00:00 2001 From: Stephan Date: Fri, 28 Oct 2016 14:33:44 +0200 Subject: [PATCH 40/48] U4-9121 - improve url perfs --- .../BulkInsertBenchmarks.cs | 9 - src/Umbraco.Tests.Benchmarks/Program.cs | 34 ++- .../TraceEvent.ReleaseNotes.txt | 61 ----- .../Umbraco.Tests.Benchmarks.csproj | 6 +- src/Umbraco.Tests.Benchmarks/XmlBenchmarks.cs | 123 +++++++++ .../PublishedContentCache.cs | 234 +++++++++--------- 6 files changed, 268 insertions(+), 199 deletions(-) delete mode 100644 src/Umbraco.Tests.Benchmarks/TraceEvent.ReleaseNotes.txt create mode 100644 src/Umbraco.Tests.Benchmarks/XmlBenchmarks.cs diff --git a/src/Umbraco.Tests.Benchmarks/BulkInsertBenchmarks.cs b/src/Umbraco.Tests.Benchmarks/BulkInsertBenchmarks.cs index 05b14a528a..4896a6570a 100644 --- a/src/Umbraco.Tests.Benchmarks/BulkInsertBenchmarks.cs +++ b/src/Umbraco.Tests.Benchmarks/BulkInsertBenchmarks.cs @@ -1,20 +1,11 @@ using System; using System.Collections.Generic; using System.Data.SqlServerCe; -using System.Diagnostics; using System.IO; -using System.Threading; -using System.Xml; using BenchmarkDotNet.Attributes; -using BenchmarkDotNet.Columns; using BenchmarkDotNet.Configs; -using BenchmarkDotNet.Diagnosers; using BenchmarkDotNet.Diagnostics.Windows; using BenchmarkDotNet.Jobs; -using BenchmarkDotNet.Loggers; -using BenchmarkDotNet.Reports; -using BenchmarkDotNet.Running; -using BenchmarkDotNet.Validators; using Umbraco.Core; using Umbraco.Core.Logging; using Umbraco.Core.Models.Rdbms; diff --git a/src/Umbraco.Tests.Benchmarks/Program.cs b/src/Umbraco.Tests.Benchmarks/Program.cs index 37c1ccd853..91ae1d37c3 100644 --- a/src/Umbraco.Tests.Benchmarks/Program.cs +++ b/src/Umbraco.Tests.Benchmarks/Program.cs @@ -1,19 +1,35 @@ using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Threading.Tasks; +using System.Reflection; using BenchmarkDotNet.Running; namespace Umbraco.Tests.Benchmarks { - class Program + internal class Program { - static void Main(string[] args) + public static void Main(string[] args) { - var summary = BenchmarkRunner.Run(); - - Console.ReadLine(); + if (args.Length == 0) + { + var summary = BenchmarkRunner.Run(); + Console.ReadLine(); + } + else if (args.Length == 1) + { + var type = Assembly.GetExecutingAssembly().GetType("Umbraco.Tests.Benchmarks." +args[0]); + if (type == null) + { + Console.WriteLine("Unknown benchmark."); + } + else + { + var summary = BenchmarkRunner.Run(type); + Console.ReadLine(); + } + } + else + { + Console.WriteLine("?"); + } } } } diff --git a/src/Umbraco.Tests.Benchmarks/TraceEvent.ReleaseNotes.txt b/src/Umbraco.Tests.Benchmarks/TraceEvent.ReleaseNotes.txt deleted file mode 100644 index 21fcb5d0ca..0000000000 --- a/src/Umbraco.Tests.Benchmarks/TraceEvent.ReleaseNotes.txt +++ /dev/null @@ -1,61 +0,0 @@ -Version 1.0.0.3 - Initial release to NuGet, pre-release. - - TraceEvent has been available from the site http://bcl.codeplex.com/wikipage?title=TraceEvent for some time now - this NuGet Version of the library supersedes that one. WHile the 'core' part of the library is unchanged, - we did change lesser used features, and change the namespace and DLL name, which will cause break. We anticipate - it will take an hour or so to 'port' to this version from the old one. Below are specific details on what - has changed to help in this port. - - * The DLL has been renamed from TraceEvent.dll to Microsoft.Diagnostics.Tracing.TraceEvent.dll - * The name spaces for all classes have been changed. The easiest way to port is to simply place - the following using clauses at the top of any file that uses TraceEvent classes - using Microsoft.Diagnostics.Symbols; - using Microsoft.Diagnostics.Tracing; - using Microsoft.Diagnostics.Tracing.Etlx; - using Microsoft.Diagnostics.Tracing.Parsers.Clr; - using Microsoft.Diagnostics.Tracing.Parsers.Kernel; - using Microsoft.Diagnostics.Tracing.Session; - using Microsoft.Diagnostics.Tracing.Stacks; - * Any method with the name RelMSec in it has been changed to be RelativeMSec. The easiest port is to - simply globally rename RelMSec to RelativeMSec - * Any property in the Trace* classes that has the form Max*Index has been renamed to Count. - * A number of methods have been declared obsolete, these are mostly renames and the warning will tell you - how to update them. - * The following classes have been rename - SymPath -> SymbolPath - SymPathElement -> SymbolPathElement - SymbolReaderFlags -> SymbolReaderOptions - * TraceEventSession is now StopOnDispose (it will stop the session when TraceEventSesssion dies), by default - If you were relying on the kernel session living past the process that started it, you must now set - the StopOnDispose explicitly - * There used to be XmlAttrib extensions methods on StringBuilder for use in manifest generated TraceEventParsers - These have been moved to protected members of TraceEvent. The result is that in stead of writing - sb.XmlAttrib(...) you write XmlAttrib(sb, ...) - * References to Pdb in names have been replaced with 'Symbol' to conform to naming guidelines. - - *********************************************************************************************** -Version 1.0.0.4 - Initial stable release - - Mostly this was insuring that the library was cleaned up in preparation - for release the TraceParserGen tool - - Improved the docs, removed old code, fixed some naming convention stuff - - * Additional changes from the PreRelease copy to the first Stable release - - * The arguments to AddCallbackForProviderEvent were reversed!!!! (now provider than event) - * The arguments to Observe(string, string)!!!! (now provider than event) - * Event names for these APIs must include a / between the Task and Opcode names - - * Many Events in KernelTraceEventParser were harmonized to be consistent with other conventions - * Events of the form PageFault* were typically renamed to Memory* - * The 'End' suffix was renamed to 'Stop' (its official name) - * PerfInfoSampleProf -> PerfInfoSample - * PerfInfoSampleProf -> PerfInfoSample - * ReadyThread -> DispatcherReadyThread - * StackWalkTraceData -> StackWalkStackTraceData - * FileIo -> FileIO - * DiskIo -> DiskIO - - * Many Events in SymbolTraceEventParser were harmonized to be consistent with other conventions - * names with Symbol -> ImageID diff --git a/src/Umbraco.Tests.Benchmarks/Umbraco.Tests.Benchmarks.csproj b/src/Umbraco.Tests.Benchmarks/Umbraco.Tests.Benchmarks.csproj index 66033ba08e..c8477d5e8c 100644 --- a/src/Umbraco.Tests.Benchmarks/Umbraco.Tests.Benchmarks.csproj +++ b/src/Umbraco.Tests.Benchmarks/Umbraco.Tests.Benchmarks.csproj @@ -99,20 +99,16 @@ + - - - - - {31785bc3-256c-4613-b2f5-a1b0bdded8c1} diff --git a/src/Umbraco.Tests.Benchmarks/XmlBenchmarks.cs b/src/Umbraco.Tests.Benchmarks/XmlBenchmarks.cs new file mode 100644 index 0000000000..c12545ec94 --- /dev/null +++ b/src/Umbraco.Tests.Benchmarks/XmlBenchmarks.cs @@ -0,0 +1,123 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using System.Xml; +using BenchmarkDotNet.Attributes; +using BenchmarkDotNet.Configs; +using BenchmarkDotNet.Diagnostics.Windows; +using BenchmarkDotNet.Jobs; + +namespace Umbraco.Tests.Benchmarks +{ + [Config(typeof(Config))] + public class XmlBenchmarks + { + private class Config : ManualConfig + { + public Config() + { + Add(new MemoryDiagnoser()); + //Add(ExecutionValidator.FailOnError); + + //The 'quick and dirty' settings, so it runs a little quicker + // see benchmarkdotnet FAQ + Add(Job.Default + .WithLaunchCount(1) // benchmark process will be launched only once + .WithIterationTime(100) // 100ms per iteration + .WithWarmupCount(3) // 3 warmup iteration + .WithTargetCount(3)); // 3 target iteration + } + } + + [Setup] + public void Setup() + { + var templateId = 0; + var xmlText = @" + + + + +]> + + + + + 1 + + This is some content
]]> + + + + + + + + + + + + + + + + +"; + _xml = new XmlDocument(); + _xml.LoadXml(xmlText); + } + + [Cleanup] + public void Cleanup() + { + _xml = null; + } + + private XmlDocument _xml; + + [Benchmark] + public void XmlWithXPath() + { + var xpath = "/root/* [@isDoc and @urlName='home']//* [@isDoc and @urlName='sub1']//* [@isDoc and @urlName='sub2']"; + var elt = _xml.SelectSingleNode(xpath); + if (elt == null) Console.WriteLine("ERR"); + } + + [Benchmark] + public void XmlWithNavigation() + { + var elt = _xml.DocumentElement; + var id = NavigateElementRoute(elt, new[] {"home", "sub1", "sub2"}); + if (id <= 0) Console.WriteLine("ERR"); + } + + private const bool UseLegacySchema = false; + + private int NavigateElementRoute(XmlElement elt, string[] urlParts) + { + var found = true; + var i = 0; + while (found && i < urlParts.Length) + { + found = false; + foreach (XmlElement child in elt.ChildNodes) + { + var noNode = UseLegacySchema + ? child.Name != "node" + : child.GetAttributeNode("isDoc") == null; + if (noNode) continue; + if (child.GetAttribute("urlName") != urlParts[i]) continue; + + found = true; + elt = child; + break; + } + i++; + } + return found ? int.Parse(elt.GetAttribute("id")) : -1; + } + } +} diff --git a/src/Umbraco.Web/PublishedCache/XmlPublishedCache/PublishedContentCache.cs b/src/Umbraco.Web/PublishedCache/XmlPublishedCache/PublishedContentCache.cs index 884138a9b4..83a4606d19 100644 --- a/src/Umbraco.Web/PublishedCache/XmlPublishedCache/PublishedContentCache.cs +++ b/src/Umbraco.Web/PublishedCache/XmlPublishedCache/PublishedContentCache.cs @@ -16,7 +16,9 @@ using umbraco; using System.Linq; using umbraco.BusinessLogic; using umbraco.presentation.preview; +using Umbraco.Core.Services; using GlobalSettings = umbraco.GlobalSettings; +using Task = System.Threading.Tasks.Task; namespace Umbraco.Web.PublishedCache.XmlPublishedCache { @@ -26,6 +28,13 @@ namespace Umbraco.Web.PublishedCache.XmlPublishedCache private readonly RoutesCache _routesCache = new RoutesCache(!UnitTesting); + private DomainHelper _domainHelper; + + private DomainHelper GetDomainHelper(IDomainService domainService) + { + return _domainHelper ?? (_domainHelper = new DomainHelper(domainService)); + } + // for INTERNAL, UNIT TESTS use ONLY internal RoutesCache RoutesCache { get { return _routesCache; } } @@ -99,6 +108,13 @@ namespace Umbraco.Web.PublishedCache.XmlPublishedCache // - non-colliding, adds one complete "by route" lookup, only on the first time a url is computed (then it's cached anyways) // - colliding, adds one "by route" lookup, the first time the url is computed, then one dictionary looked each time it is computed again // assuming no collisions, the impact is one complete "by route" lookup the first time each url is computed + // + // U4-9121 - this lookup is too expensive when computing a large amount of urls on a front-end (eg menu) + // ... thinking about moving the lookup out of the path into its own async task, so we are not reporting errors + // in the back-office anymore, but at least we are not polluting the cache + // instead, refactored DeterminedIdByRoute to stop using XPath, with a 16x improvement according to benchmarks + // will it be enough? + var loopId = preview ? 0 : _routesCache.GetNodeId(route); // might be cached already in case of collision if (loopId == 0) { @@ -130,62 +146,141 @@ namespace Umbraco.Web.PublishedCache.XmlPublishedCache var pos = route.IndexOf('/'); var path = pos == 0 ? route : route.Substring(pos); var startNodeId = pos == 0 ? 0 : int.Parse(route.Substring(0, pos)); - IEnumerable vars; - - var xpath = CreateXpathQuery(startNodeId, path, hideTopLevelNode, out vars); //check if we can find the node in our xml cache - var content = GetSingleByXPath(umbracoContext, preview, xpath, vars == null ? null : vars.ToArray()); + var id = NavigateRoute(umbracoContext, preview, startNodeId, path, hideTopLevelNode); + if (id > 0) return GetById(umbracoContext, preview, id); // if hideTopLevelNodePath is true then for url /foo we looked for /*/foo // but maybe that was the url of a non-default top-level node, so we also // have to look for /foo (see note in ApplyHideTopLevelNodeFromPath). - if (content == null && hideTopLevelNode && path.Length > 1 && path.IndexOf('/', 1) < 0) + if (hideTopLevelNode && path.Length > 1 && path.IndexOf('/', 1) < 0) { - xpath = CreateXpathQuery(startNodeId, path, false, out vars); - content = GetSingleByXPath(umbracoContext, preview, xpath, vars == null ? null : vars.ToArray()); + var id2 = NavigateRoute(umbracoContext, preview, startNodeId, path, false); + if (id2 > 0) return GetById(umbracoContext, preview, id2); } - return content; + return null; + } + + private int NavigateRoute(UmbracoContext umbracoContext, bool preview, int startNodeId, string path, bool hideTopLevelNode) + { + var xml = GetXml(umbracoContext, preview); + XmlElement elt; + + // empty path + if (path == string.Empty || path == "/") + { + if (startNodeId > 0) + { + elt = xml.GetElementById(startNodeId.ToString(CultureInfo.InvariantCulture)); + return elt == null ? -1 : startNodeId; + } + + elt = null; + var min = int.MaxValue; + foreach (XmlElement e in xml.DocumentElement.ChildNodes) + { + var sortOrder = int.Parse(e.GetAttribute("sortOrder")); + if (sortOrder < min) + { + min = sortOrder; + elt = e; + } + } + return elt == null ? -1 : int.Parse(elt.GetAttribute("id")); + } + + // non-empty path + elt = startNodeId <= 0 + ? xml.DocumentElement + : xml.GetElementById(startNodeId.ToString(CultureInfo.InvariantCulture)); + if (elt == null) return -1; + + var urlParts = path.Split(SlashChar, StringSplitOptions.RemoveEmptyEntries); + + if (hideTopLevelNode && startNodeId <= 0) + { + foreach (XmlElement e in elt.ChildNodes) + { + var id = NavigateElementRoute(e, urlParts); + if (id > 0) return id; + } + return -1; + } + + return NavigateElementRoute(elt, urlParts); + } + + private static bool UseLegacySchema + { + get { return UmbracoConfig.For.UmbracoSettings().Content.UseLegacyXmlSchema; } + } + + private int NavigateElementRoute(XmlElement elt, string[] urlParts) + { + var found = true; + var i = 0; + while (found && i < urlParts.Length) + { + found = false; + foreach (XmlElement child in elt.ChildNodes) + { + var noNode = UseLegacySchema + ? child.Name != "node" + : child.GetAttributeNode("isDoc") == null; + if (noNode) continue; + if (child.GetAttribute("urlName") != urlParts[i]) continue; + + found = true; + elt = child; + break; + } + i++; + } + return found ? int.Parse(elt.GetAttribute("id")) : -1; } string DetermineRouteById(UmbracoContext umbracoContext, bool preview, int contentId) { - var node = GetById(umbracoContext, preview, contentId); - if (node == null) - return null; + var elt = GetXml(umbracoContext, preview).GetElementById(contentId.ToString(CultureInfo.InvariantCulture)); + if (elt == null) return null; - var domainHelper = new DomainHelper(umbracoContext.Application.Services.DomainService); + var domainHelper = GetDomainHelper(umbracoContext.Application.Services.DomainService); // walk up from that node until we hit a node with a domain, // or we reach the content root, collecting urls in the way var pathParts = new List(); - var n = node; - var hasDomains = domainHelper.NodeHasDomains(n.Id); - while (hasDomains == false && n != null) // n is null at root + var eltId = int.Parse(elt.GetAttribute("id")); + var eltParentId = int.Parse(((XmlElement) elt.ParentNode).GetAttribute("id")); + var e = elt; + var id = eltId; + var hasDomains = domainHelper.NodeHasDomains(id); + while (hasDomains == false && id != -1) { // get the url - var urlName = n.UrlName; + var urlName = e.GetAttribute("urlName"); pathParts.Add(urlName); // move to parent node - n = n.Parent; - hasDomains = n != null && domainHelper.NodeHasDomains(n.Id); + e = (XmlElement) e.ParentNode; + id = int.Parse(e.GetAttribute("id")); + hasDomains = id != -1 && domainHelper.NodeHasDomains(id); } // no domain, respect HideTopLevelNodeFromPath for legacy purposes - if (hasDomains == false && global::umbraco.GlobalSettings.HideTopLevelNodeFromPath) - ApplyHideTopLevelNodeFromPath(umbracoContext, node, pathParts); + if (hasDomains == false && GlobalSettings.HideTopLevelNodeFromPath) + ApplyHideTopLevelNodeFromPath(umbracoContext, eltId, eltParentId, pathParts); // assemble the route pathParts.Reverse(); var path = "/" + string.Join("/", pathParts); // will be "/" or "/foo" or "/foo/bar" etc - var route = (n == null ? "" : n.Id.ToString(CultureInfo.InvariantCulture)) + path; + var route = (id == -1 ? "" : id.ToString(CultureInfo.InvariantCulture)) + path; return route; } - static void ApplyHideTopLevelNodeFromPath(UmbracoContext umbracoContext, IPublishedContent node, IList pathParts) + static void ApplyHideTopLevelNodeFromPath(UmbracoContext umbracoContext, int nodeId, int parentId, IList pathParts) { // in theory if hideTopLevelNodeFromPath is true, then there should be only once // top-level node, or else domains should be assigned. but for backward compatibility @@ -195,12 +290,12 @@ namespace Umbraco.Web.PublishedCache.XmlPublishedCache // "/foo" fails (looking for "/*/foo") we try also "/foo". // this does not make much sense anyway esp. if both "/foo/" and "/bar/foo" exist, but // that's the way it works pre-4.10 and we try to be backward compat for the time being - if (node.Parent == null) + if (parentId == -1) { var rootNode = umbracoContext.ContentCache.GetByRoute("/", true); if (rootNode == null) throw new Exception("Failed to get node at /."); - if (rootNode.Id == node.Id) // remove only if we're the default node + if (rootNode.Id == nodeId) // remove only if we're the default node pathParts.RemoveAt(pathParts.Count - 1); } else @@ -217,12 +312,7 @@ namespace Umbraco.Web.PublishedCache.XmlPublishedCache { public int Version { get; private set; } - public static string Root { get { return "/root"; } } public string RootDocuments { get; private set; } - public string DescendantDocumentById { get; private set; } - public string ChildDocumentByUrlName { get; private set; } - public string ChildDocumentByUrlNameVar { get; private set; } - public string RootDocumentWithLowestSortOrder { get; private set; } public XPathStringsDefinition(int version) { @@ -233,19 +323,11 @@ namespace Umbraco.Web.PublishedCache.XmlPublishedCache // legacy XML schema case 0: RootDocuments = "/root/node"; - DescendantDocumentById = "//node [@id={0}]"; - ChildDocumentByUrlName = "/node [@urlName='{0}']"; - ChildDocumentByUrlNameVar = "/node [@urlName=${0}]"; - RootDocumentWithLowestSortOrder = "/root/node [not(@sortOrder > ../node/@sortOrder)][1]"; break; // default XML schema as of 4.10 case 1: RootDocuments = "/root/* [@isDoc]"; - DescendantDocumentById = "//* [@isDoc and @id={0}]"; - ChildDocumentByUrlName = "/* [@isDoc and @urlName='{0}']"; - ChildDocumentByUrlNameVar = "/* [@isDoc and @urlName=${0}]"; - RootDocumentWithLowestSortOrder = "/root/* [@isDoc and not(@sortOrder > ../* [@isDoc]/@sortOrder)][1]"; break; default: @@ -421,84 +503,6 @@ namespace Umbraco.Web.PublishedCache.XmlPublishedCache static readonly char[] SlashChar = new[] { '/' }; - protected string CreateXpathQuery(int startNodeId, string path, bool hideTopLevelNodeFromPath, out IEnumerable vars) - { - string xpath; - vars = null; - - if (path == string.Empty || path == "/") - { - // if url is empty - if (startNodeId > 0) - { - // if in a domain then use the root node of the domain - xpath = string.Format(XPathStringsDefinition.Root + XPathStrings.DescendantDocumentById, startNodeId); - } - else - { - // if not in a domain - what is the default page? - // let's say it is the first one in the tree, if any -- order by sortOrder - - // but! - // umbraco does not consistently guarantee that sortOrder starts with 0 - // so the one that we want is the one with the smallest sortOrder - // read http://stackoverflow.com/questions/1128745/how-can-i-use-xpath-to-find-the-minimum-value-of-an-attribute-in-a-set-of-elemen - - // so that one does not work, because min(@sortOrder) maybe 1 - // xpath = "/root/*[@isDoc and @sortOrder='0']"; - - // and we can't use min() because that's XPath 2.0 - // that one works - xpath = XPathStrings.RootDocumentWithLowestSortOrder; - } - } - else - { - // if url is not empty, then use it to try lookup a matching page - var urlParts = path.Split(SlashChar, StringSplitOptions.RemoveEmptyEntries); - var xpathBuilder = new StringBuilder(); - int partsIndex = 0; - List varsList = null; - - if (startNodeId == 0) - { - if (hideTopLevelNodeFromPath) - xpathBuilder.Append(XPathStrings.RootDocuments); // first node is not in the url - else - xpathBuilder.Append(XPathStringsDefinition.Root); - } - else - { - xpathBuilder.AppendFormat(XPathStringsDefinition.Root + XPathStrings.DescendantDocumentById, startNodeId); - // always "hide top level" when there's a domain - } - - while (partsIndex < urlParts.Length) - { - var part = urlParts[partsIndex++]; - if (part.Contains('\'') || part.Contains('"')) - { - // use vars, escaping gets ugly pretty quickly - varsList = varsList ?? new List(); - var varName = string.Format("var{0}", partsIndex); - varsList.Add(new XPathVariable(varName, part)); - xpathBuilder.AppendFormat(XPathStrings.ChildDocumentByUrlNameVar, varName); - } - else - { - xpathBuilder.AppendFormat(XPathStrings.ChildDocumentByUrlName, part); - - } - } - - xpath = xpathBuilder.ToString(); - if (varsList != null) - vars = varsList.ToArray(); - } - - return xpath; - } - #endregion #region Detached From c2d3d057a5d4a55bdb6ff1721637273ec07f30be Mon Sep 17 00:00:00 2001 From: Stephan Date: Wed, 2 Nov 2016 13:35:32 +0100 Subject: [PATCH 41/48] U4-9137 - Fix PublishedMediaCache getting trashed media --- .../PublishedCache/XmlPublishedCache/PublishedMediaCache.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Umbraco.Web/PublishedCache/XmlPublishedCache/PublishedMediaCache.cs b/src/Umbraco.Web/PublishedCache/XmlPublishedCache/PublishedMediaCache.cs index da1ff94fe1..f3cc101b21 100644 --- a/src/Umbraco.Web/PublishedCache/XmlPublishedCache/PublishedMediaCache.cs +++ b/src/Umbraco.Web/PublishedCache/XmlPublishedCache/PublishedMediaCache.cs @@ -226,7 +226,7 @@ namespace Umbraco.Web.PublishedCache.XmlPublishedCache // the media from the service, first var media = ApplicationContext.Current.Services.MediaService.GetById(id); - if (media == null) return null; // not found, ok + if (media == null || media.Trashed) return null; // not found, ok // so, the media was not found in Examine's index *yet* it exists, which probably indicates that // the index is corrupted. Or not up-to-date. Log a warning, but only once, and only if seeing the From 0ff26aa442e365c4e78323874d7471aff9dc7c3e Mon Sep 17 00:00:00 2001 From: David Peck Date: Thu, 3 Nov 2016 10:50:10 +0000 Subject: [PATCH 42/48] (U4-8183) Changed the link picker dialog from Page Title to Link title in en and en_us --- src/Umbraco.Web.UI/umbraco/config/lang/en.xml | 2 +- src/Umbraco.Web.UI/umbraco/config/lang/en_us.xml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Umbraco.Web.UI/umbraco/config/lang/en.xml b/src/Umbraco.Web.UI/umbraco/config/lang/en.xml index f710e54215..6859004c22 100644 --- a/src/Umbraco.Web.UI/umbraco/config/lang/en.xml +++ b/src/Umbraco.Web.UI/umbraco/config/lang/en.xml @@ -144,7 +144,7 @@ Role Member Type No date chosen - Page Title + Link Title Properties This document is published but is not visible because the parent '%0%' is unpublished This document is published but is not in the cache diff --git a/src/Umbraco.Web.UI/umbraco/config/lang/en_us.xml b/src/Umbraco.Web.UI/umbraco/config/lang/en_us.xml index 80c6faf07b..547090b19f 100644 --- a/src/Umbraco.Web.UI/umbraco/config/lang/en_us.xml +++ b/src/Umbraco.Web.UI/umbraco/config/lang/en_us.xml @@ -145,7 +145,7 @@ Role Member Type No date chosen - Page Title + Link Title Properties This document is published but is not visible because the parent '%0%' is unpublished This document is published but is not in the cache From 9162575b35d83fc6c8e7015c4d03384518069f8b Mon Sep 17 00:00:00 2001 From: David Peck Date: Thu, 3 Nov 2016 10:53:33 +0000 Subject: [PATCH 43/48] (U4-8183) Changed the casing to match other labels --- src/Umbraco.Web.UI/umbraco/config/lang/en.xml | 2 +- src/Umbraco.Web.UI/umbraco/config/lang/en_us.xml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Umbraco.Web.UI/umbraco/config/lang/en.xml b/src/Umbraco.Web.UI/umbraco/config/lang/en.xml index 6859004c22..67515e96a5 100644 --- a/src/Umbraco.Web.UI/umbraco/config/lang/en.xml +++ b/src/Umbraco.Web.UI/umbraco/config/lang/en.xml @@ -144,7 +144,7 @@ Role Member Type No date chosen - Link Title + Link title Properties This document is published but is not visible because the parent '%0%' is unpublished This document is published but is not in the cache diff --git a/src/Umbraco.Web.UI/umbraco/config/lang/en_us.xml b/src/Umbraco.Web.UI/umbraco/config/lang/en_us.xml index 547090b19f..6c976947bb 100644 --- a/src/Umbraco.Web.UI/umbraco/config/lang/en_us.xml +++ b/src/Umbraco.Web.UI/umbraco/config/lang/en_us.xml @@ -145,7 +145,7 @@ Role Member Type No date chosen - Link Title + Link title Properties This document is published but is not visible because the parent '%0%' is unpublished This document is published but is not in the cache From 1b2d75ee52782e238b6459c017664f39ae4217b8 Mon Sep 17 00:00:00 2001 From: Chris Houston Date: Thu, 3 Nov 2016 11:43:03 +0000 Subject: [PATCH 44/48] Fixed the font being used in the Media Picker when a file has been selected. --- src/Umbraco.Web.UI.Client/src/less/property-editors.less | 4 ++-- .../src/views/propertyeditors/mediapicker/mediapicker.html | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/Umbraco.Web.UI.Client/src/less/property-editors.less b/src/Umbraco.Web.UI.Client/src/less/property-editors.less index 16d48807eb..573f218fb1 100644 --- a/src/Umbraco.Web.UI.Client/src/less/property-editors.less +++ b/src/Umbraco.Web.UI.Client/src/less/property-editors.less @@ -232,11 +232,11 @@ ul.color-picker li a { max-height: none !important; } -.umb-sortable-thumbnails .icon-holder { +.umb-sortable-thumbnails .umb-icon-holder { text-align: center; } -.umb-sortable-thumbnails .icon-holder .icon{ +.umb-sortable-thumbnails .umb-icon-holder .icon{ font-size: 40px; line-height: 50px; color: @gray; diff --git a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/mediapicker/mediapicker.html b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/mediapicker/mediapicker.html index 68d2fc6f4d..f3aab992dd 100644 --- a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/mediapicker/mediapicker.html +++ b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/mediapicker/mediapicker.html @@ -11,7 +11,7 @@ - + {{image.name}} From c1ffefc6af3687bd4b77cc0f128cf73dea19f078 Mon Sep 17 00:00:00 2001 From: Stefano Chiodino Date: Thu, 3 Nov 2016 11:40:20 +0000 Subject: [PATCH 45/48] U4-9055 remove background-color on preview that was interfering with the website style --- src/Umbraco.Web.UI.Client/src/less/canvas-designer.less | 1 - 1 file changed, 1 deletion(-) diff --git a/src/Umbraco.Web.UI.Client/src/less/canvas-designer.less b/src/Umbraco.Web.UI.Client/src/less/canvas-designer.less index 52c4052975..e0527aefcb 100644 --- a/src/Umbraco.Web.UI.Client/src/less/canvas-designer.less +++ b/src/Umbraco.Web.UI.Client/src/less/canvas-designer.less @@ -21,7 +21,6 @@ body { font-size: 14px; line-height: 20px; color: #343434; - background-color: #F5F5F5; -webkit-transition: all 0.2s ease-in-out; -moz-transition: all 0.2s ease-in-out; transition: all 0.2s ease-in-out; From ae659e3c84aaf86623895fc6767da114f1db331c Mon Sep 17 00:00:00 2001 From: "CORP\\Rachel.Breeze" Date: Thu, 3 Nov 2016 13:40:46 +0000 Subject: [PATCH 46/48] U4-7239 Now trims the parameters so that if a default is set in a comma and space seperated list, the parameter is now populated. --- src/Umbraco.Web/umbraco.presentation/helper.cs | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/Umbraco.Web/umbraco.presentation/helper.cs b/src/Umbraco.Web/umbraco.presentation/helper.cs index 0d133fee66..2bee9d1e3a 100644 --- a/src/Umbraco.Web/umbraco.presentation/helper.cs +++ b/src/Umbraco.Web/umbraco.presentation/helper.cs @@ -95,12 +95,14 @@ namespace umbraco { var attributeValueSplit = (attributeValue).Split(','); + attributeValueSplit = attributeValueSplit.Select(x => x.Trim()).ToArray(); + // before proceeding, we don't want to process anything here unless each item starts/ends with a [ ] // this is because the attribute value could actually just be a json array like [1,2,3] which we don't want to parse // // however, the last one can be a literal, must take care of this! // so here, don't check the last one, which can be just anything - if (attributeValueSplit.Take(attributeValueSplit.Length - 1).All(x => + if (attributeValueSplit.Take(attributeValueSplit.Length - 1).All(x => //must end with [ x.EndsWith("]") && //must start with [ and a special char From c8b23195f6ebadeeba83d58fd91f59b1e4ca32b9 Mon Sep 17 00:00:00 2001 From: Lotte Pitcher Date: Thu, 3 Nov 2016 14:30:38 +0000 Subject: [PATCH 47/48] When truncating if the last characters was the first half of a two word unicode character, also include the second half to avoid YSOD --- src/Umbraco.Web/HtmlStringUtilities.cs | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/src/Umbraco.Web/HtmlStringUtilities.cs b/src/Umbraco.Web/HtmlStringUtilities.cs index 5ba1d17f4e..24a643b5b0 100644 --- a/src/Umbraco.Web/HtmlStringUtilities.cs +++ b/src/Umbraco.Web/HtmlStringUtilities.cs @@ -211,6 +211,13 @@ namespace Umbraco.Web if (!lengthReached && currentTextLength >= length) { + // if the last character added was the first of a two character unicode pair, add the second character + if (Char.IsHighSurrogate((char)ic)) + { + var lowSurrogate = tr.Read(); + outputtw.Write((char)lowSurrogate); + } + // Reached truncate limit. if (addElipsis) { From 74e0a4d9996738c5c621664c580f769ebf44126e Mon Sep 17 00:00:00 2001 From: Stephan Date: Mon, 31 Oct 2016 11:08:28 +0100 Subject: [PATCH 48/48] 7.6 merge review fixes --- src/Umbraco.Core/ByteArrayExtensions.cs | 39 ++ src/Umbraco.Core/ICompletable.cs | 9 + .../IO/FileSystemProviderManager.cs | 2 +- src/Umbraco.Core/IO/IFileSystem.cs | 10 +- src/Umbraco.Core/IO/MediaFileSystem.cs | 657 +++++++++++++++++- src/Umbraco.Core/IO/ShadowFileSystem.cs | 2 +- src/Umbraco.Core/IO/ShadowFileSystemsScope.cs | 2 +- src/Umbraco.Core/IO/ShadowWrapper.cs | 3 +- src/Umbraco.Core/IO/UmbracoMediaFile.cs | 6 +- src/Umbraco.Core/MainDom.cs | 7 +- src/Umbraco.Core/Media/ImageExtensions.cs | 21 + src/Umbraco.Core/Media/ImageHelper.cs | 383 ---------- src/Umbraco.Core/Media/MediaHelper.cs | 281 -------- .../Media/UploadAutoFillProperties.cs | 38 +- src/Umbraco.Core/Models/ContentExtensions.cs | 6 +- src/Umbraco.Core/Models/PublicAccessEntry.cs | 5 +- .../Packaging/PackageBinaryInspector.cs | 20 +- .../Persistence/DefaultDatabaseFactory.cs | 2 +- .../Repositories/VersionableRepositoryBase.cs | 4 +- src/Umbraco.Core/Services/MediaService.cs | 14 +- src/Umbraco.Core/Umbraco.Core.csproj | 5 +- src/Umbraco.Tests/IO/ShadowFileSystemTests.cs | 34 +- src/Umbraco.Tests/TestHelpers/TestHelper.cs | 45 ++ src/Umbraco.Web/Editors/ImagesController.cs | 4 +- .../FileUploadPropertyEditor.cs | 28 +- .../FileUploadPropertyValueEditor.cs | 35 +- .../ImageCropperPropertyEditor.cs | 29 +- .../ImageCropperPropertyValueEditor.cs | 35 +- src/umbraco.cms/businesslogic/Content.cs | 2 +- .../businesslogic/datatype/FileHandlerData.cs | 2 +- 30 files changed, 895 insertions(+), 835 deletions(-) create mode 100644 src/Umbraco.Core/ByteArrayExtensions.cs create mode 100644 src/Umbraco.Core/ICompletable.cs create mode 100644 src/Umbraco.Core/Media/ImageExtensions.cs delete mode 100644 src/Umbraco.Core/Media/ImageHelper.cs delete mode 100644 src/Umbraco.Core/Media/MediaHelper.cs diff --git a/src/Umbraco.Core/ByteArrayExtensions.cs b/src/Umbraco.Core/ByteArrayExtensions.cs new file mode 100644 index 0000000000..dacdd509ca --- /dev/null +++ b/src/Umbraco.Core/ByteArrayExtensions.cs @@ -0,0 +1,39 @@ +namespace Umbraco.Core +{ + public static class ByteArrayExtensions + { + private static readonly char[] BytesToHexStringLookup = { '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'A', 'B', 'C', 'D', 'E', 'F' }; + + public static string ToHexString(this byte[] bytes) + { + int i = 0, p = 0, bytesLength = bytes.Length; + var chars = new char[bytesLength * 2]; + while (i < bytesLength) + { + var b = bytes[i++]; + chars[p++] = BytesToHexStringLookup[b / 0x10]; + chars[p++] = BytesToHexStringLookup[b % 0x10]; + } + return new string(chars, 0, chars.Length); + } + + public static string ToHexString(this byte[] bytes, char separator, int blockSize, int blockCount) + { + int p = 0, bytesLength = bytes.Length, count = 0, size = 0; + var chars = new char[bytesLength * 2 + blockCount]; + for (var i = 0; i < bytesLength; i++) + { + var b = bytes[i++]; + chars[p++] = BytesToHexStringLookup[b / 0x10]; + chars[p++] = BytesToHexStringLookup[b % 0x10]; + if (count == blockCount) continue; + if (++size < blockSize) continue; + + chars[p++] = '/'; + size = 0; + count++; + } + return new string(chars, 0, chars.Length); + } + } +} diff --git a/src/Umbraco.Core/ICompletable.cs b/src/Umbraco.Core/ICompletable.cs new file mode 100644 index 0000000000..594d82b0ae --- /dev/null +++ b/src/Umbraco.Core/ICompletable.cs @@ -0,0 +1,9 @@ +using System; + +namespace Umbraco.Core +{ + public interface ICompletable : IDisposable + { + void Complete(); + } +} diff --git a/src/Umbraco.Core/IO/FileSystemProviderManager.cs b/src/Umbraco.Core/IO/FileSystemProviderManager.cs index 1e076bb979..d807832bcb 100644 --- a/src/Umbraco.Core/IO/FileSystemProviderManager.cs +++ b/src/Umbraco.Core/IO/FileSystemProviderManager.cs @@ -204,7 +204,7 @@ namespace Umbraco.Core.IO // _shadowEnabled = true; //} - public ShadowFileSystemsScope Shadow(Guid id) + public ICompletable Shadow(Guid id) { var typed = _wrappers.ToArray(); var wrappers = new ShadowWrapper[typed.Length + 7]; diff --git a/src/Umbraco.Core/IO/IFileSystem.cs b/src/Umbraco.Core/IO/IFileSystem.cs index 063f63c437..003b4891f5 100644 --- a/src/Umbraco.Core/IO/IFileSystem.cs +++ b/src/Umbraco.Core/IO/IFileSystem.cs @@ -4,9 +4,6 @@ using System.IO; namespace Umbraco.Core.IO { - //TODO: There is no way to create a directory here without creating a file in a directory and then deleting it - //TODO: Should probably implement a rename? - public interface IFileSystem { IEnumerable GetDirectories(string path); @@ -46,5 +43,12 @@ namespace Umbraco.Core.IO public interface IFileSystem2 : IFileSystem { long GetSize(string path); + + // TODO: implement these + // + //void CreateDirectory(string path); + // + //// move or rename, directory or file + //void Move(string source, string target); } } diff --git a/src/Umbraco.Core/IO/MediaFileSystem.cs b/src/Umbraco.Core/IO/MediaFileSystem.cs index d0e1bbee53..6f32ef6da0 100644 --- a/src/Umbraco.Core/IO/MediaFileSystem.cs +++ b/src/Umbraco.Core/IO/MediaFileSystem.cs @@ -1,10 +1,18 @@ using System; using System.Collections.Generic; +using System.Drawing; +using System.Drawing.Drawing2D; +using System.Drawing.Imaging; using System.Globalization; using System.IO; +using System.Linq; +using System.Threading; using Umbraco.Core.Configuration; using Umbraco.Core.Configuration.UmbracoSettings; +using Umbraco.Core.Logging; using Umbraco.Core.Media; +using Umbraco.Core.Media.Exif; +using Umbraco.Core.Models; namespace Umbraco.Core.IO { @@ -15,17 +23,35 @@ namespace Umbraco.Core.IO public class MediaFileSystem : FileSystemWrapper { private readonly IContentSection _contentConfig; + private readonly UploadAutoFillProperties _uploadAutoFillProperties; - public MediaFileSystem(IFileSystem2 wrapped) - : this(wrapped, UmbracoConfig.For.UmbracoSettings().Content) - { - } + private readonly object _folderCounterLock = new object(); + private long _folderCounter; + private bool _folderCounterInitialized; - public MediaFileSystem(IFileSystem2 wrapped, IContentSection contentConfig) : base(wrapped) + private static readonly Dictionary DefaultSizes = new Dictionary + { + { 100, "thumb" }, + { 500, "big-thumb" } + }; + + public MediaFileSystem(IFileSystem wrapped) + : this(wrapped, UmbracoConfig.For.UmbracoSettings().Content, ApplicationContext.Current.ProfilingLogger.Logger) + { } + + public MediaFileSystem(IFileSystem wrapped, IContentSection contentConfig, ILogger logger) + : base(wrapped) { _contentConfig = contentConfig; + _uploadAutoFillProperties = new UploadAutoFillProperties(this, logger, contentConfig); } + internal UploadAutoFillProperties UploadAutoFillProperties { get { return _uploadAutoFillProperties; } } + + // note - this is currently experimental / being developed + //public static bool UseTheNewMediaPathScheme { get; set; } + public const bool UseTheNewMediaPathScheme = false; + // none of the methods below are used in Core anymore [Obsolete("This low-level method should NOT exist.")] @@ -48,28 +74,611 @@ namespace Umbraco.Core.IO return subfolder + sep + fileName; } - [Obsolete("Use ImageHelper.GetThumbnails instead.", false)] - public IEnumerable GetThumbnails(string path) + #region Media Path + + /// + /// Gets the file path of a media file. + /// + /// The file name. + /// The unique identifier of the content/media owning the file. + /// The unique identifier of the property type owning the file. + /// The filesystem-relative path to the media file. + /// With the old media path scheme, this CREATES a new media path each time it is invoked. + public string GetMediaPath(string filename, Guid cuid, Guid puid) { - return ImageHelper.GetThumbnails(this, path); - } + filename = Path.GetFileName(filename); + if (filename == null) throw new ArgumentException("Cannot become a safe filename.", "filename"); + filename = IOHelper.SafeFileName(filename.ToLowerInvariant()); - [Obsolete("Use ImageHelper.DeleteFile instead.", false)] - public void DeleteFile(string path, bool deleteThumbnails) - { - ImageHelper.DeleteFile(this, path, deleteThumbnails); - } + string folder; + if (UseTheNewMediaPathScheme == false) + { + // old scheme: filepath is "/" OR "-" + // default media filesystem maps to "~/media/" + folder = GetNextFolder(); + } + else + { + // new scheme: path is "-/" OR "--" + // default media filesystem maps to "~/media/" + // assumes that cuid and puid keys can be trusted - and that a single property type + // for a single content cannot store two different files with the same name + folder = Combine(cuid, puid).ToHexString(/*'/', 2, 4*/); // could use ext to fragment path eg 12/e4/f2/... + } - [Obsolete("Use ImageHelper.DeleteThumbnails instead.", false)] - public void DeleteThumbnails(string path) - { - ImageHelper.DeleteThumbnails(this, path); - } + var filepath = UmbracoConfig.For.UmbracoSettings().Content.UploadAllowDirectories + ? Path.Combine(folder, filename) + : folder + "-" + filename; - [Obsolete("Use ImageHelper.CopyThumbnails instead.", false)] - public void CopyThumbnails(string sourcePath, string targetPath) - { - ImageHelper.CopyThumbnails(this, sourcePath, targetPath); + return filepath; } - } + + private static byte[] Combine(Guid guid1, Guid guid2) + { + var bytes1 = guid1.ToByteArray(); + var bytes2 = guid2.ToByteArray(); + var bytes = new byte[bytes1.Length]; + for (var i = 0; i < bytes1.Length; i++) + bytes[i] = (byte)(bytes1[i] ^ bytes2[i]); + return bytes; + } + + /// + /// Gets the file path of a media file. + /// + /// The file name. + /// A previous file path. + /// The unique identifier of the content/media owning the file. + /// The unique identifier of the property type owning the file. + /// The filesystem-relative path to the media file. + /// In the old, legacy, number-based scheme, we try to re-use the media folder + /// specified by . Else, we CREATE a new one. Each time we are invoked. + public string GetMediaPath(string filename, string prevpath, Guid cuid, Guid puid) + { + if (UseTheNewMediaPathScheme || string.IsNullOrWhiteSpace(prevpath)) + return GetMediaPath(filename, cuid, puid); + + filename = Path.GetFileName(filename); + if (filename == null) throw new ArgumentException("Cannot become a safe filename.", "filename"); + filename = IOHelper.SafeFileName(filename.ToLowerInvariant()); + + // old scheme, with a previous path + // prevpath should be "/" OR "-" + // and we want to reuse the "" part, so try to find it + + var sep = UmbracoConfig.For.UmbracoSettings().Content.UploadAllowDirectories ? "/" : "-"; + var pos = prevpath.IndexOf(sep, StringComparison.Ordinal); + var s = pos > 0 ? prevpath.Substring(0, pos) : null; + int ignored; + + var folder = (pos > 0 && int.TryParse(s, out ignored)) ? s : GetNextFolder(); + + // ReSharper disable once AssignNullToNotNullAttribute + var filepath = UmbracoConfig.For.UmbracoSettings().Content.UploadAllowDirectories + ? Path.Combine(folder, filename) + : folder + "-" + filename; + + return filepath; + } + + /// + /// Gets the next media folder in the original number-based scheme. + /// + /// + /// Should be private, is internal for legacy FileHandlerData which is obsolete. + internal string GetNextFolder() + { + EnsureFolderCounterIsInitialized(); + return Interlocked.Increment(ref _folderCounter).ToString(CultureInfo.InvariantCulture); + } + + private void EnsureFolderCounterIsInitialized() + { + lock (_folderCounterLock) + { + if (_folderCounterInitialized) return; + + _folderCounter = 1000; // seed + var directories = GetDirectories(""); + foreach (var directory in directories) + { + long folderNumber; + if (long.TryParse(directory, out folderNumber) && folderNumber > _folderCounter) + _folderCounter = folderNumber; + } + + // note: not multi-domains ie LB safe as another domain could create directories + // while we read and parse them - don't fix, move to new scheme eventually + + _folderCounterInitialized = true; + } + } + + #endregion + + #region Associated Media Files + + /// + /// Stores a media file. + /// + /// The content item owning the media file. + /// The property type owning the media file. + /// The media file name. + /// A stream containing the media bytes. + /// An optional filesystem-relative filepath to the previous media file. + /// The filesystem-relative filepath to the media file. + /// + /// The file is considered "owned" by the content/propertyType. + /// If an is provided then that file (and thumbnails) is deleted + /// before the new file is saved, and depending on the media path scheme, the folder + /// may be reused for the new file. + /// + public string StoreFile(IContentBase content, PropertyType propertyType, string filename, Stream filestream, string oldpath) + { + if (content == null) throw new ArgumentNullException("content"); + if (propertyType == null) throw new ArgumentNullException("propertyType"); + if (string.IsNullOrWhiteSpace(filename)) throw new ArgumentException("Null or empty.", "filename"); + if (filestream == null) throw new ArgumentNullException("filestream"); + + // clear the old file, if any + if (string.IsNullOrWhiteSpace(oldpath) == false) + DeleteFile(oldpath, true); + + // get the filepath, store the data + // use oldpath as "prevpath" to try and reuse the folder, in original number-based scheme + var filepath = GetMediaPath(filename, oldpath, content.Key, propertyType.Key); + AddFile(filepath, filestream); + return filepath; + } + + /// + /// Clears a media file. + /// + /// The filesystem-relative path to the media file. + public new void DeleteFile(string filepath) + { + DeleteFile(filepath, true); + } + + /// + /// Copies a media file. + /// + /// The content item owning the copy of the media file. + /// The property type owning the copy of the media file. + /// The filesystem-relative path to the source media file. + /// The filesystem-relative path to the copy of the media file. + public string CopyFile(IContentBase content, PropertyType propertyType, string sourcepath) + { + if (content == null) throw new ArgumentNullException("content"); + if (propertyType == null) throw new ArgumentNullException("propertyType"); + if (string.IsNullOrWhiteSpace(sourcepath)) throw new ArgumentException("Null or empty.", "sourcepath"); + + // ensure we have a file to copy + if (FileExists(sourcepath) == false) return null; + + // get the filepath + var filename = Path.GetFileName(sourcepath); + var filepath = GetMediaPath(filename, content.Key, propertyType.Key); + this.CopyFile(sourcepath, filepath); + CopyThumbnails(sourcepath, filepath); + return filepath; + } + + /// + /// Gets or creates a property for a content item. + /// + /// The content item. + /// The property type alias. + /// The property. + private static Property GetProperty(IContentBase content, string propertyTypeAlias) + { + var property = content.Properties.FirstOrDefault(x => x.Alias.InvariantEquals(propertyTypeAlias)); + if (property != null) return property; + + var propertyType = content.GetContentType().CompositionPropertyTypes + .FirstOrDefault(x => x.Alias.InvariantEquals(propertyTypeAlias)); + if (propertyType == null) + throw new Exception("No property type exists with alias " + propertyTypeAlias + "."); + + property = new Property(propertyType); + content.Properties.Add(property); + return property; + } + + public void SetUploadFile(IContentBase content, string propertyTypeAlias, string filename, Stream filestream) + { + var property = GetProperty(content, propertyTypeAlias); + var svalue = property.Value as string; + var oldpath = svalue == null ? null : GetRelativePath(svalue); + var filepath = StoreFile(content, property.PropertyType, filename, filestream, oldpath); + property.Value = GetUrl(filepath); + SetUploadFile(content, property, filepath, filestream); + } + + public void SetUploadFile(IContentBase content, string propertyTypeAlias, string filepath) + { + var property = GetProperty(content, propertyTypeAlias); + var svalue = property.Value as string; + var oldpath = svalue == null ? null : GetRelativePath(svalue); // FIXME DELETE? + if (string.IsNullOrWhiteSpace(oldpath) == false && oldpath != filepath) + DeleteFile(oldpath, true); + property.Value = GetUrl(filepath); + using (var filestream = OpenFile(filepath)) + { + SetUploadFile(content, property, filepath, filestream); + } + } + + // sets a file for the FileUpload property editor + // ie generates thumbnails and populates autofill properties + private void SetUploadFile(IContentBase content, Property property, string filepath, Stream filestream) + { + // check if file is an image (and supports resizing and thumbnails etc) + var extension = Path.GetExtension(filepath); + var isImage = IsImageFile(extension); + + // specific stuff for images (thumbnails etc) + if (isImage) + { + using (var image = Image.FromStream(filestream)) + { + // use one image for all + GenerateThumbnails(image, filepath, property.PropertyType); + _uploadAutoFillProperties.Populate(content, property.Alias, filepath, filestream, image); + } + } + else + { + // will use filepath for extension, and filestream for length + _uploadAutoFillProperties.Populate(content, property.Alias, filepath, filestream); + } + } + + #endregion + + #region Image + + /// + /// Gets a value indicating whether the file extension corresponds to an image. + /// + /// The file extension. + /// A value indicating whether the file extension corresponds to an image. + public bool IsImageFile(string extension) + { + if (extension == null) return false; + extension = extension.TrimStart('.'); + return UmbracoConfig.For.UmbracoSettings().Content.ImageFileTypes.InvariantContains(extension); + } + + /// + /// Gets the dimensions of an image. + /// + /// A stream containing the image bytes. + /// The dimension of the image. + /// First try with EXIF as it is faster and does not load the entire image + /// in memory. Fallback to GDI which means loading the image in memory and thus + /// use potentially large amounts of memory. + public Size GetDimensions(Stream stream) + { + //Try to load with exif + try + { + var jpgInfo = ImageFile.FromStream(stream); + + if (jpgInfo.Format != ImageFileFormat.Unknown + && jpgInfo.Properties.ContainsKey(ExifTag.PixelYDimension) + && jpgInfo.Properties.ContainsKey(ExifTag.PixelXDimension)) + { + var height = Convert.ToInt32(jpgInfo.Properties[ExifTag.PixelYDimension].Value); + var width = Convert.ToInt32(jpgInfo.Properties[ExifTag.PixelXDimension].Value); + if (height > 0 && width > 0) + { + return new Size(width, height); + } + } + } + catch (Exception) + { + //We will just swallow, just means we can't read exif data, we don't want to log an error either + } + + //we have no choice but to try to read in via GDI + using (var image = Image.FromStream(stream)) + { + + var fileWidth = image.Width; + var fileHeight = image.Height; + return new Size(fileWidth, fileHeight); + } + } + + #endregion + + #region Manage thumbnails + + // note: this does not find 'custom' thumbnails? + // will find _thumb and _big-thumb but NOT _custom? + public IEnumerable GetThumbnails(string path) + { + var parentDirectory = Path.GetDirectoryName(path); + var extension = Path.GetExtension(path); + + return GetFiles(parentDirectory) + .Where(x => x.StartsWith(path.TrimEnd(extension) + "_thumb") || x.StartsWith(path.TrimEnd(extension) + "_big-thumb")) + .ToList(); + } + + public void DeleteFile(string path, bool deleteThumbnails) + { + base.DeleteFile(path); + + if (deleteThumbnails == false) + return; + + DeleteThumbnails(path); + } + + public void DeleteThumbnails(string path) + { + GetThumbnails(path) + .ForEach(x => base.DeleteFile(x)); + } + + public void CopyThumbnails(string sourcePath, string targetPath) + { + var targetPathBase = Path.GetDirectoryName(targetPath) ?? ""; + foreach (var sourceThumbPath in GetThumbnails(sourcePath)) + { + var sourceThumbFilename = Path.GetFileName(sourceThumbPath) ?? ""; + var targetThumbPath = Path.Combine(targetPathBase, sourceThumbFilename); + this.CopyFile(sourceThumbPath, targetThumbPath); + } + } + + #endregion + + #region GenerateThumbnails + + public IEnumerable GenerateThumbnails( + Image image, + string filepath, + string preValue) + { + if (string.IsNullOrWhiteSpace(preValue)) + return GenerateThumbnails(image, filepath); + + var additionalSizes = new List(); + var sep = preValue.Contains(",") ? "," : ";"; + var values = preValue.Split(new[] { sep }, StringSplitOptions.RemoveEmptyEntries); + foreach (var value in values) + { + int size; + if (int.TryParse(value, out size)) + additionalSizes.Add(size); + } + + return GenerateThumbnails(image, filepath, additionalSizes); + } + + public IEnumerable GenerateThumbnails( + Image image, + string filepath, + IEnumerable additionalSizes = null) + { + var w = image.Width; + var h = image.Height; + + var sizes = additionalSizes == null ? DefaultSizes.Keys : DefaultSizes.Keys.Concat(additionalSizes); + + // start with default sizes, + // add additional sizes, + // filter out duplicates, + // filter out those that would be larger that the original image + // and create the thumbnail + return sizes + .Distinct() + .Where(x => w >= x && h >= x) + .Select(x => GenerateResized(image, filepath, DefaultSizes.ContainsKey(x) ? DefaultSizes[x] : "", x)) + .ToList(); // now + } + + public IEnumerable GenerateThumbnails( + Stream filestream, + string filepath, + PropertyType propertyType) + { + // get the original image from the original stream + if (filestream.CanSeek) filestream.Seek(0, 0); // fixme - what if we cannot seek? + using (var image = Image.FromStream(filestream)) + { + return GenerateThumbnails(image, filepath, propertyType); + } + } + + public IEnumerable GenerateThumbnails( + Image image, + string filepath, + PropertyType propertyType) + { + // if the editor is an upload field, check for additional thumbnail sizes + // that can be defined in the prevalue for the property data type. otherwise, + // just use the default sizes. + var sizes = propertyType.PropertyEditorAlias == Constants.PropertyEditors.UploadFieldAlias + ? ApplicationContext.Current.Services.DataTypeService + .GetPreValuesByDataTypeId(propertyType.DataTypeDefinitionId) + .FirstOrDefault() + : string.Empty; + + return GenerateThumbnails(image, filepath, sizes); + } + + #endregion + + #region GenerateResized - Generate at resized filepath derived from origin filepath + + public ResizedImage GenerateResized(Image originImage, string originFilepath, string sizeName, int maxWidthHeight) + { + return GenerateResized(originImage, originFilepath, sizeName, maxWidthHeight, -1, -1); + } + + public ResizedImage GenerateResized(Image originImage, string originFilepath, string sizeName, int fixedWidth, int fixedHeight) + { + return GenerateResized(originImage, originFilepath, sizeName, -1, fixedWidth, fixedHeight); + } + + public ResizedImage GenerateResized(Image originImage, string originFilepath, string sizeName, int maxWidthHeight, int fixedWidth, int fixedHeight) + { + if (string.IsNullOrWhiteSpace(sizeName)) + sizeName = "UMBRACOSYSTHUMBNAIL"; + var extension = Path.GetExtension(originFilepath) ?? string.Empty; + var filebase = originFilepath.TrimEnd(extension); + var resizedFilepath = filebase + "_" + sizeName + extension; + + return GenerateResizedAt(originImage, resizedFilepath, maxWidthHeight, fixedWidth, fixedHeight); + } + + #endregion + + #region GenerateResizedAt - Generate at specified resized filepath + + public ResizedImage GenerateResizedAt(Image originImage, string resizedFilepath, int maxWidthHeight) + { + return GenerateResizedAt(originImage, resizedFilepath, maxWidthHeight, -1, -1); + } + + public ResizedImage GenerateResizedAt(Image originImage, int fixedWidth, int fixedHeight, string resizedFilepath) + { + return GenerateResizedAt(originImage, resizedFilepath, -1, fixedWidth, fixedHeight); + } + + public ResizedImage GenerateResizedAt(Image originImage, string resizedFilepath, int maxWidthHeight, int fixedWidth, int fixedHeight) + { + // target dimensions + int width, height; + + // if maxWidthHeight then get ratio + if (maxWidthHeight > 0) + { + var fx = (float)originImage.Size.Width / maxWidthHeight; + var fy = (float)originImage.Size.Height / maxWidthHeight; + var f = Math.Max(fx, fy); // fit in thumbnail size + width = (int)Math.Round(originImage.Size.Width / f); + height = (int)Math.Round(originImage.Size.Height / f); + if (width == 0) width = 1; + if (height == 0) height = 1; + } + else if (fixedWidth > 0 && fixedHeight > 0) + { + width = fixedWidth; + height = fixedHeight; + } + else + { + width = height = 1; + } + + // create new image with best quality settings + using (var bitmap = new Bitmap(width, height)) + using (var graphics = Graphics.FromImage(bitmap)) + { + // if the image size is rather large we cannot use the best quality interpolation mode + // because we'll get out of mem exceptions. So we detect how big the image is and use + // the mid quality interpolation mode when the image size exceeds our max limit. + graphics.InterpolationMode = originImage.Width > 5000 || originImage.Height > 5000 + ? InterpolationMode.Bilinear // mid quality + : InterpolationMode.HighQualityBicubic; // best quality + + // everything else is best-quality + graphics.SmoothingMode = SmoothingMode.HighQuality; + graphics.PixelOffsetMode = PixelOffsetMode.HighQuality; + graphics.CompositingQuality = CompositingQuality.HighQuality; + + // copy the old image to the new and resize + var rect = new Rectangle(0, 0, width, height); + graphics.DrawImage(originImage, rect, 0, 0, originImage.Width, originImage.Height, GraphicsUnit.Pixel); + + // copy metadata + // fixme - er... no? + + // get an encoder - based upon the file type + var extension = (Path.GetExtension(resizedFilepath) ?? "").TrimStart('.').ToLowerInvariant(); + var encoders = ImageCodecInfo.GetImageEncoders(); + ImageCodecInfo encoder; + switch (extension) + { + case "png": + encoder = encoders.Single(t => t.MimeType.Equals("image/png")); + break; + case "gif": + encoder = encoders.Single(t => t.MimeType.Equals("image/gif")); + break; + case "tif": + case "tiff": + encoder = encoders.Single(t => t.MimeType.Equals("image/tiff")); + break; + case "bmp": + encoder = encoders.Single(t => t.MimeType.Equals("image/bmp")); + break; + // TODO: this is dirty, defaulting to jpg but the return value of this thing is used all over the + // place so left it here, but it needs to not set a codec if it doesn't know which one to pick + // Note: when fixing this: both .jpg and .jpeg should be handled as extensions + default: + encoder = encoders.Single(t => t.MimeType.Equals("image/jpeg")); + break; + } + + // set compresion ratio to 90% + var encoderParams = new EncoderParameters(); + encoderParams.Param[0] = new EncoderParameter(Encoder.Quality, 90L); + + // save the new image + using (var stream = new MemoryStream()) + { + bitmap.Save(stream, encoder, encoderParams); + stream.Seek(0, 0); + if (resizedFilepath.Contains("UMBRACOSYSTHUMBNAIL")) + { + var filepath = resizedFilepath.Replace("UMBRACOSYSTHUMBNAIL", maxWidthHeight.ToInvariantString()); + AddFile(filepath, stream); + if (extension != "jpg") + { + filepath = filepath.TrimEnd(extension) + "jpg"; + stream.Seek(0, 0); + AddFile(filepath, stream); + } + // TODO: Remove this, this is ONLY here for backwards compatibility but it is essentially completely unusable see U4-5385 + stream.Seek(0, 0); + resizedFilepath = resizedFilepath.Replace("UMBRACOSYSTHUMBNAIL", width + "x" + height); + } + + AddFile(resizedFilepath, stream); + } + + return new ResizedImage(resizedFilepath, width, height); + } + } + + #endregion + + #region Inner classes + + public class ResizedImage + { + public ResizedImage() + { } + + public ResizedImage(string filepath, int width, int height) + { + Filepath = filepath; + Width = width; + Height = height; + } + + public string Filepath { get; set; } + public int Width { get; set; } + public int Height { get; set; } + } + + #endregion + } } diff --git a/src/Umbraco.Core/IO/ShadowFileSystem.cs b/src/Umbraco.Core/IO/ShadowFileSystem.cs index 276e058bcc..1e5da10bdc 100644 --- a/src/Umbraco.Core/IO/ShadowFileSystem.cs +++ b/src/Umbraco.Core/IO/ShadowFileSystem.cs @@ -5,7 +5,7 @@ using System.Linq; namespace Umbraco.Core.IO { - public class ShadowFileSystem : IFileSystem2 + internal class ShadowFileSystem : IFileSystem2 { private readonly IFileSystem _fs; private readonly IFileSystem2 _sfs; diff --git a/src/Umbraco.Core/IO/ShadowFileSystemsScope.cs b/src/Umbraco.Core/IO/ShadowFileSystemsScope.cs index 5e0280ddca..0793946b62 100644 --- a/src/Umbraco.Core/IO/ShadowFileSystemsScope.cs +++ b/src/Umbraco.Core/IO/ShadowFileSystemsScope.cs @@ -5,7 +5,7 @@ using Umbraco.Core.Logging; namespace Umbraco.Core.IO { - public class ShadowFileSystemsScope : IDisposable + internal class ShadowFileSystemsScope : ICompletable { // note: taking a reference to the _manager instead of using manager.Current // to avoid using Current everywhere but really, we support only 1 scope at diff --git a/src/Umbraco.Core/IO/ShadowWrapper.cs b/src/Umbraco.Core/IO/ShadowWrapper.cs index 7f9bc61a66..87d2b8db9f 100644 --- a/src/Umbraco.Core/IO/ShadowWrapper.cs +++ b/src/Umbraco.Core/IO/ShadowWrapper.cs @@ -2,11 +2,10 @@ using System.Collections.Generic; using System.IO; using System.Linq; -using System.Web.Hosting; namespace Umbraco.Core.IO { - public class ShadowWrapper : IFileSystem2 + internal class ShadowWrapper : IFileSystem2 { private readonly IFileSystem _innerFileSystem; private readonly string _shadowPath; diff --git a/src/Umbraco.Core/IO/UmbracoMediaFile.cs b/src/Umbraco.Core/IO/UmbracoMediaFile.cs index 8e1d8c0987..18438831e2 100644 --- a/src/Umbraco.Core/IO/UmbracoMediaFile.cs +++ b/src/Umbraco.Core/IO/UmbracoMediaFile.cs @@ -119,7 +119,7 @@ namespace Umbraco.Core.IO public bool SupportsResizing { - get { return ImageHelper.IsImageFile(Extension); } + get { return _fs.IsImageFile(Extension); } } public string GetFriendlyName() @@ -137,7 +137,7 @@ namespace Umbraco.Core.IO using (var fs = _fs.OpenFile(Path)) { - _size = ImageHelper.GetDimensions(fs); + _size = _fs.GetDimensions(fs); } } else @@ -171,7 +171,7 @@ namespace Umbraco.Core.IO using (var filestream = _fs.OpenFile(Path)) using (var image = Image.FromStream(filestream)) { - return ImageHelper.GenerateResized(_fs, image, Path, sizeName, maxWidthHeight, width, height).Filepath; + return _fs.GenerateResized(image, Path, sizeName, maxWidthHeight, width, height).Filepath; } } diff --git a/src/Umbraco.Core/MainDom.cs b/src/Umbraco.Core/MainDom.cs index 52dbb21f21..e5d63cb6c9 100644 --- a/src/Umbraco.Core/MainDom.cs +++ b/src/Umbraco.Core/MainDom.cs @@ -1,13 +1,8 @@ using System; using System.Collections.Generic; -using System.Diagnostics; -using System.IO.MemoryMappedFiles; -using System.Text; using System.Threading; -using System.Threading.Tasks; using System.Web.Hosting; using Umbraco.Core.Logging; -using Umbraco.Core.ObjectResolution; namespace Umbraco.Core { @@ -109,7 +104,7 @@ namespace Umbraco.Core /// An action to execute before the AppDomain releases the main domain status. /// An optional weight (lower goes first). /// A value indicating whether it was possible to register. - /// If registering is successful, then the action + /// If registering is successful, then the action /// is guaranteed to execute before the AppDomain releases the main domain status. public bool Register(Action install, Action release, int weight = 100) { diff --git a/src/Umbraco.Core/Media/ImageExtensions.cs b/src/Umbraco.Core/Media/ImageExtensions.cs new file mode 100644 index 0000000000..c20be13d31 --- /dev/null +++ b/src/Umbraco.Core/Media/ImageExtensions.cs @@ -0,0 +1,21 @@ +using System.Drawing; +using System.Drawing.Imaging; +using System.Linq; + +namespace Umbraco.Core.Media +{ + public static class ImageExtensions + { + /// + /// Gets the MIME type of an image. + /// + /// The image. + /// The MIME type of the image. + public static string GetMimeType(this Image image) + { + var format = image.RawFormat; + var codec = ImageCodecInfo.GetImageDecoders().First(c => c.FormatID == format.Guid); + return codec.MimeType; + } + } +} diff --git a/src/Umbraco.Core/Media/ImageHelper.cs b/src/Umbraco.Core/Media/ImageHelper.cs deleted file mode 100644 index f3cab2d3b0..0000000000 --- a/src/Umbraco.Core/Media/ImageHelper.cs +++ /dev/null @@ -1,383 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Drawing; -using System.Drawing.Drawing2D; -using System.Drawing.Imaging; -using System.IO; -using System.Linq; -using Umbraco.Core.Configuration; -using Umbraco.Core.IO; -using Umbraco.Core.Media.Exif; -using Umbraco.Core.Models; - -namespace Umbraco.Core.Media -{ - /// - /// Provides helper methods for managing images. - /// - public static class ImageHelper - { - private static readonly Dictionary DefaultSizes = new Dictionary - { - { 100, "thumb" }, - { 500, "big-thumb" } - }; - - /// - /// Gets a value indicating whether the file extension corresponds to an image. - /// - /// The file extension. - /// A value indicating whether the file extension corresponds to an image. - public static bool IsImageFile(string extension) - { - if (extension == null) return false; - extension = extension.TrimStart('.'); - return UmbracoConfig.For.UmbracoSettings().Content.ImageFileTypes.InvariantContains(extension); - } - - /// - /// Gets the dimensions of an image. - /// - /// A stream containing the image bytes. - /// The dimension of the image. - /// First try with EXIF as it is faster and does not load the entire image - /// in memory. Fallback to GDI which means loading the image in memory and thus - /// use potentially large amounts of memory. - public static Size GetDimensions(Stream stream) - { - //Try to load with exif - try - { - var jpgInfo = ImageFile.FromStream(stream); - - if (jpgInfo.Format != ImageFileFormat.Unknown - && jpgInfo.Properties.ContainsKey(ExifTag.PixelYDimension) - && jpgInfo.Properties.ContainsKey(ExifTag.PixelXDimension)) - { - var height = Convert.ToInt32(jpgInfo.Properties[ExifTag.PixelYDimension].Value); - var width = Convert.ToInt32(jpgInfo.Properties[ExifTag.PixelXDimension].Value); - if (height > 0 && width > 0) - { - return new Size(width, height); - } - } - } - catch (Exception) - { - //We will just swallow, just means we can't read exif data, we don't want to log an error either - } - - //we have no choice but to try to read in via GDI - using (var image = Image.FromStream(stream)) - { - - var fileWidth = image.Width; - var fileHeight = image.Height; - return new Size(fileWidth, fileHeight); - } - } - - /// - /// Gets the MIME type of an image. - /// - /// The image. - /// The MIME type of the image. - public static string GetMimeType(this Image image) - { - var format = image.RawFormat; - var codec = ImageCodecInfo.GetImageDecoders().First(c => c.FormatID == format.Guid); - return codec.MimeType; - } - - #region Manage thumbnails - - // note: this does not find 'custom' thumbnails? - // will find _thumb and _big-thumb but NOT _custom? - public static IEnumerable GetThumbnails(IFileSystem fs, string path) - { - var parentDirectory = Path.GetDirectoryName(path); - var extension = Path.GetExtension(path); - - return fs.GetFiles(parentDirectory) - .Where(x => x.StartsWith(path.TrimEnd(extension) + "_thumb") || x.StartsWith(path.TrimEnd(extension) + "_big-thumb")) - .ToList(); - } - - public static void DeleteFile(IFileSystem fs, string path, bool deleteThumbnails) - { - fs.DeleteFile(path); - - if (deleteThumbnails == false) - return; - - DeleteThumbnails(fs, path); - } - - public static void DeleteThumbnails(IFileSystem fs, string path) - { - GetThumbnails(fs, path) - .ForEach(fs.DeleteFile); - } - - public static void CopyThumbnails(IFileSystem fs, string sourcePath, string targetPath) - { - var targetPathBase = Path.GetDirectoryName(targetPath) ?? ""; - foreach (var sourceThumbPath in GetThumbnails(fs, sourcePath)) - { - var sourceThumbFilename = Path.GetFileName(sourceThumbPath) ?? ""; - var targetThumbPath = Path.Combine(targetPathBase, sourceThumbFilename); - fs.CopyFile(sourceThumbPath, targetThumbPath); - } - } - - #endregion - - #region GenerateThumbnails - - public static IEnumerable GenerateThumbnails( - IFileSystem fs, - Image image, - string filepath, - string preValue) - { - if (string.IsNullOrWhiteSpace(preValue)) - return GenerateThumbnails(fs, image, filepath); - - var additionalSizes = new List(); - var sep = preValue.Contains(",") ? "," : ";"; - var values = preValue.Split(new[] { sep }, StringSplitOptions.RemoveEmptyEntries); - foreach (var value in values) - { - int size; - if (int.TryParse(value, out size)) - additionalSizes.Add(size); - } - - return GenerateThumbnails(fs, image, filepath, additionalSizes); - } - - public static IEnumerable GenerateThumbnails( - IFileSystem fs, - Image image, - string filepath, - IEnumerable additionalSizes = null) - { - var w = image.Width; - var h = image.Height; - - var sizes = additionalSizes == null ? DefaultSizes.Keys : DefaultSizes.Keys.Concat(additionalSizes); - - // start with default sizes, - // add additional sizes, - // filter out duplicates, - // filter out those that would be larger that the original image - // and create the thumbnail - return sizes - .Distinct() - .Where(x => w >= x && h >= x) - .Select(x => GenerateResized(fs, image, filepath, DefaultSizes.ContainsKey(x) ? DefaultSizes[x] : "", x)) - .ToList(); // now - } - - public static IEnumerable GenerateThumbnails( - IFileSystem fs, - Stream filestream, - string filepath, - PropertyType propertyType) - { - // get the original image from the original stream - if (filestream.CanSeek) filestream.Seek(0, 0); // fixme - what if we cannot seek? - using (var image = Image.FromStream(filestream)) - { - return GenerateThumbnails(fs, image, filepath, propertyType); - } - } - - public static IEnumerable GenerateThumbnails( - IFileSystem fs, - Image image, - string filepath, - PropertyType propertyType) - { - // if the editor is an upload field, check for additional thumbnail sizes - // that can be defined in the prevalue for the property data type. otherwise, - // just use the default sizes. - var sizes = propertyType.PropertyEditorAlias == Constants.PropertyEditors.UploadFieldAlias - ? ApplicationContext.Current.Services.DataTypeService - .GetPreValuesByDataTypeId(propertyType.DataTypeDefinitionId) - .FirstOrDefault() - : string.Empty; - - return GenerateThumbnails(fs, image, filepath, sizes); - } - - #endregion - - #region GenerateResized - Generate at resized filepath derived from origin filepath - - public static ResizedImage GenerateResized(IFileSystem fs, Image originImage, string originFilepath, string sizeName, int maxWidthHeight) - { - return GenerateResized(fs, originImage, originFilepath, sizeName, maxWidthHeight, -1, -1); - } - - public static ResizedImage GenerateResized(IFileSystem fs, Image originImage, string originFilepath, string sizeName, int fixedWidth, int fixedHeight) - { - return GenerateResized(fs, originImage, originFilepath, sizeName, -1, fixedWidth, fixedHeight); - } - - public static ResizedImage GenerateResized(IFileSystem fs, Image originImage, string originFilepath, string sizeName, int maxWidthHeight, int fixedWidth, int fixedHeight) - { - if (string.IsNullOrWhiteSpace(sizeName)) - sizeName = "UMBRACOSYSTHUMBNAIL"; - var extension = Path.GetExtension(originFilepath) ?? string.Empty; - var filebase = originFilepath.TrimEnd(extension); - var resizedFilepath = filebase + "_" + sizeName + extension; - - return GenerateResizedAt(fs, originImage, resizedFilepath, maxWidthHeight, fixedWidth, fixedHeight); - } - - #endregion - - #region GenerateResizedAt - Generate at specified resized filepath - - public static ResizedImage GenerateResizedAt(IFileSystem fs, Image originImage, string resizedFilepath, int maxWidthHeight) - { - return GenerateResizedAt(fs, originImage, resizedFilepath, maxWidthHeight, -1, -1); - } - - public static ResizedImage GenerateResizedAt(IFileSystem fs, Image originImage, int fixedWidth, int fixedHeight, string resizedFilepath) - { - return GenerateResizedAt(fs, originImage, resizedFilepath, -1, fixedWidth, fixedHeight); - } - - public static ResizedImage GenerateResizedAt(IFileSystem fs, Image originImage, string resizedFilepath, int maxWidthHeight, int fixedWidth, int fixedHeight) - { - // target dimensions - int width, height; - - // if maxWidthHeight then get ratio - if (maxWidthHeight > 0) - { - var fx = (float) originImage.Size.Width / maxWidthHeight; - var fy = (float) originImage.Size.Height / maxWidthHeight; - var f = Math.Max(fx, fy); // fit in thumbnail size - width = (int) Math.Round(originImage.Size.Width / f); - height = (int) Math.Round(originImage.Size.Height / f); - if (width == 0) width = 1; - if (height == 0) height = 1; - } - else if (fixedWidth > 0 && fixedHeight > 0) - { - width = fixedWidth; - height = fixedHeight; - } - else - { - width = height = 1; - } - - // create new image with best quality settings - using (var bitmap = new Bitmap(width, height)) - using (var graphics = Graphics.FromImage(bitmap)) - { - // if the image size is rather large we cannot use the best quality interpolation mode - // because we'll get out of mem exceptions. So we detect how big the image is and use - // the mid quality interpolation mode when the image size exceeds our max limit. - graphics.InterpolationMode = originImage.Width > 5000 || originImage.Height > 5000 - ? InterpolationMode.Bilinear // mid quality - : InterpolationMode.HighQualityBicubic; // best quality - - // everything else is best-quality - graphics.SmoothingMode = SmoothingMode.HighQuality; - graphics.PixelOffsetMode = PixelOffsetMode.HighQuality; - graphics.CompositingQuality = CompositingQuality.HighQuality; - - // copy the old image to the new and resize - var rect = new Rectangle(0, 0, width, height); - graphics.DrawImage(originImage, rect, 0, 0, originImage.Width, originImage.Height, GraphicsUnit.Pixel); - - // copy metadata - // fixme - er... no? - - // get an encoder - based upon the file type - var extension = (Path.GetExtension(resizedFilepath) ?? "").TrimStart('.').ToLowerInvariant(); - var encoders = ImageCodecInfo.GetImageEncoders(); - ImageCodecInfo encoder; - switch (extension) - { - case "png": - encoder = encoders.Single(t => t.MimeType.Equals("image/png")); - break; - case "gif": - encoder = encoders.Single(t => t.MimeType.Equals("image/gif")); - break; - case "tif": - case "tiff": - encoder = encoders.Single(t => t.MimeType.Equals("image/tiff")); - break; - case "bmp": - encoder = encoders.Single(t => t.MimeType.Equals("image/bmp")); - break; - // TODO: this is dirty, defaulting to jpg but the return value of this thing is used all over the - // place so left it here, but it needs to not set a codec if it doesn't know which one to pick - // Note: when fixing this: both .jpg and .jpeg should be handled as extensions - default: - encoder = encoders.Single(t => t.MimeType.Equals("image/jpeg")); - break; - } - - // set compresion ratio to 90% - var encoderParams = new EncoderParameters(); - encoderParams.Param[0] = new EncoderParameter(Encoder.Quality, 90L); - - // save the new image - using (var stream = new MemoryStream()) - { - bitmap.Save(stream, encoder, encoderParams); - stream.Seek(0, 0); - if (resizedFilepath.Contains("UMBRACOSYSTHUMBNAIL")) - { - var filepath = resizedFilepath.Replace("UMBRACOSYSTHUMBNAIL", maxWidthHeight.ToInvariantString()); - fs.AddFile(filepath, stream); - if (extension != "jpg") - { - filepath = filepath.TrimEnd(extension) + "jpg"; - stream.Seek(0, 0); - fs.AddFile(filepath, stream); - } - // TODO: Remove this, this is ONLY here for backwards compatibility but it is essentially completely unusable see U4-5385 - stream.Seek(0, 0); - resizedFilepath = resizedFilepath.Replace("UMBRACOSYSTHUMBNAIL", width + "x" + height); - } - - fs.AddFile(resizedFilepath, stream); - } - - return new ResizedImage(resizedFilepath, width, height); - } - } - - #endregion - - #region Inner classes - - public class ResizedImage - { - public ResizedImage() - { } - - public ResizedImage(string filepath, int width, int height) - { - Filepath = filepath; - Width = width; - Height = height; - } - - public string Filepath { get; set; } - public int Width { get; set; } - public int Height { get; set; } - } - - #endregion - } -} diff --git a/src/Umbraco.Core/Media/MediaHelper.cs b/src/Umbraco.Core/Media/MediaHelper.cs deleted file mode 100644 index 040cfd5d7d..0000000000 --- a/src/Umbraco.Core/Media/MediaHelper.cs +++ /dev/null @@ -1,281 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Diagnostics; -using System.Drawing; -using System.Globalization; -using System.IO; -using System.Linq; -using System.Text; -using System.Threading; -using System.Threading.Tasks; -using Umbraco.Core.Configuration; -using Umbraco.Core.IO; -using Umbraco.Core.Models; - -namespace Umbraco.Core.Media -{ - /// - /// Provides helper methods for managing medias. - /// - /// Medias can be anything that can be uploaded via an upload - /// property, including but not limited to, images. See ImageHelper for - /// image-specific methods. - internal static class MediaHelper - { - private static long _folderCounter; - private static bool _folderCounterInitialized; - private static readonly object FolderCounterLock = new object(); - - // fixme - should be a config option of some sort! - //public static bool UseTheNewMediaPathScheme { get; set; } - public const bool UseTheNewMediaPathScheme = false; - - public static MediaFileSystem FileSystem { get { return FileSystemProviderManager.Current.GetFileSystemProvider(); } } - - #region Media Path - - /// - /// Gets the file path of a media file. - /// - /// The file name. - /// The unique identifier of the content/media owning the file. - /// The unique identifier of the property type owning the file. - /// The filesystem-relative path to the media file. - /// With the old media path scheme, this CREATES a new media path each time it is invoked. - public static string GetMediaPath(string filename, Guid cuid, Guid puid) - { - filename = Path.GetFileName(filename); - if (filename == null) throw new ArgumentException("Cannot become a safe filename.", "filename"); - filename = IOHelper.SafeFileName(filename.ToLowerInvariant()); - - string folder; - if (UseTheNewMediaPathScheme == false) - { - // old scheme: filepath is "/" OR "-" - // default media filesystem maps to "~/media/" - folder = GetNextFolder(); - } - else - { - // new scheme: path is "-/" OR "--" - // default media filesystem maps to "~/media/" - // fixme - this assumes that the keys exists and won't change (even when creating a new content) - // fixme - this is going to create looooong filepaths, any chance we can shorten them? - folder = cuid.ToString("N") + "-" + puid.ToString("N"); - } - - var filepath = UmbracoConfig.For.UmbracoSettings().Content.UploadAllowDirectories - ? Path.Combine(folder, filename) - : folder + "-" + filename; - - return filepath; - } - - /// - /// Gets the file path of a media file. - /// - /// The file name. - /// A previous file path. - /// The unique identifier of the content/media owning the file. - /// The unique identifier of the property type owning the file. - /// The filesystem-relative path to the media file. - /// In the old, legacy, number-based scheme, we try to re-use the media folder - /// specified by . Else, we CREATE a new one. Each time we are invoked. - public static string GetMediaPath(string filename, string prevpath, Guid cuid, Guid puid) - { - if (UseTheNewMediaPathScheme || string.IsNullOrWhiteSpace(prevpath)) - return GetMediaPath(filename, cuid, puid); - - filename = Path.GetFileName(filename); - if (filename == null) throw new ArgumentException("Cannot become a safe filename.", "filename"); - filename = IOHelper.SafeFileName(filename.ToLowerInvariant()); - - // old scheme, with a previous path - // prevpath should be "/" OR "-" - // and we want to reuse the "" part, so try to find it - - var sep = UmbracoConfig.For.UmbracoSettings().Content.UploadAllowDirectories ? "/" : "-"; - var pos = prevpath.IndexOf(sep, StringComparison.Ordinal); - var s = pos > 0 ? prevpath.Substring(0, pos) : null; - int ignored; - - var folder = (pos > 0 && int.TryParse(s, out ignored)) ? s : GetNextFolder(); - - // ReSharper disable once AssignNullToNotNullAttribute - var filepath = UmbracoConfig.For.UmbracoSettings().Content.UploadAllowDirectories - ? Path.Combine(folder, filename) - : folder + "-" + filename; - - return filepath; - } - - /// - /// Gets the next media folder in the original number-based scheme. - /// - /// - /// Should be private, is internal for legacy FileHandlerData which is obsolete. - internal static string GetNextFolder() - { - lock (FolderCounterLock) - { - if (_folderCounterInitialized == false) - { - _folderCounter = 1000; // seed - was not respected in MediaSubfolderCounter? - var fs = FileSystemProviderManager.Current.GetFileSystemProvider(); - var directories = fs.GetDirectories(""); - foreach (var directory in directories) - { - long folderNumber; - if (long.TryParse(directory, out folderNumber) && folderNumber > _folderCounter) - _folderCounter = folderNumber; - } - - _folderCounterInitialized = true; - } - } - - return Interlocked.Increment(ref _folderCounter).ToString(CultureInfo.InvariantCulture); - } - - #endregion - - /// - /// Stores a media file. - /// - /// The content item owning the media file. - /// The property type owning the media file. - /// The media file name. - /// A stream containing the media bytes. - /// An optional filesystem-relative filepath to the previous media file. - /// The filesystem-relative filepath to the media file. - /// - /// The file is considered "owned" by the content/propertyType. - /// If an is provided then that file (and thumbnails) is deleted - /// before the new file is saved, and depending on the media path scheme, the folder - /// may be reused for the new file. - /// - public static string StoreFile(IContentBase content, PropertyType propertyType, string filename, Stream filestream, string oldpath) - { - if (content == null) throw new ArgumentNullException("content"); - if (propertyType == null) throw new ArgumentNullException("propertyType"); - if (string.IsNullOrWhiteSpace(filename)) throw new ArgumentException("Null or empty.", "filename"); - if (filestream == null) throw new ArgumentNullException("filestream"); - - // clear the old file, if any - var fs = FileSystem; - if (string.IsNullOrWhiteSpace(oldpath) == false) - ImageHelper.DeleteFile(fs, oldpath, true); - - // get the filepath, store the data - // use oldpath as "prevpath" to try and reuse the folder, in original number-based scheme - var filepath = GetMediaPath(filename, oldpath, content.Key, propertyType.Key); - fs.AddFile(filepath, filestream); - return filepath; - } - - /// - /// Clears a media file. - /// - /// The filesystem-relative path to the media file. - public static void DeleteFile(string filepath) - { - ImageHelper.DeleteFile(FileSystem, filepath, true); - } - - /// - /// Copies a media file. - /// - /// The content item owning the copy of the media file. - /// The property type owning the copy of the media file. - /// The filesystem-relative path to the source media file. - /// The filesystem-relative path to the copy of the media file. - public static string CopyFile(IContentBase content, PropertyType propertyType, string sourcepath) - { - if (content == null) throw new ArgumentNullException("content"); - if (propertyType == null) throw new ArgumentNullException("propertyType"); - if (string.IsNullOrWhiteSpace(sourcepath)) throw new ArgumentException("Null or empty.", "sourcepath"); - - // ensure we have a file to copy - var fs = FileSystem; - if (fs.FileExists(sourcepath) == false) return null; - - // get the filepath - var filename = Path.GetFileName(sourcepath); - var filepath = GetMediaPath(filename, content.Key, propertyType.Key); - fs.CopyFile(sourcepath, filepath); - ImageHelper.CopyThumbnails(fs, sourcepath, filepath); - return filepath; - } - - /// - /// Gets or creates a property for a content item. - /// - /// The content item. - /// The property type alias. - /// The property. - private static Property GetProperty(IContentBase content, string propertyTypeAlias) - { - var property = content.Properties.FirstOrDefault(x => x.Alias.InvariantEquals(propertyTypeAlias)); - if (property != null) return property; - - var propertyType = content.GetContentType().CompositionPropertyTypes - .FirstOrDefault(x => x.Alias.InvariantEquals(propertyTypeAlias)); - if (propertyType == null) - throw new Exception("No property type exists with alias " + propertyTypeAlias + "."); - - property = new Property(propertyType); - content.Properties.Add(property); - return property; - } - - public static void SetUploadFile(IContentBase content, string propertyTypeAlias, string filename, Stream filestream) - { - var property = GetProperty(content, propertyTypeAlias); - var svalue = property.Value as string; - var oldpath = svalue == null ? null : FileSystem.GetRelativePath(svalue); - var filepath = StoreFile(content, property.PropertyType, filename, filestream, oldpath); - property.Value = FileSystem.GetUrl(filepath); - SetUploadFile(content, property, FileSystem, filepath, filestream); - } - - public static void SetUploadFile(IContentBase content, string propertyTypeAlias, string filepath) - { - var property = GetProperty(content, propertyTypeAlias); - var svalue = property.Value as string; - var oldpath = svalue == null ? null : FileSystem.GetRelativePath(svalue); // FIXME DELETE? - if (string.IsNullOrWhiteSpace(oldpath) == false && oldpath != filepath) - FileSystem.DeleteFile(oldpath); - property.Value = FileSystem.GetUrl(filepath); - var fs = FileSystem; - using (var filestream = fs.OpenFile(filepath)) - { - SetUploadFile(content, property, fs, filepath, filestream); - } - } - - // sets a file for the FileUpload property editor - // ie generates thumbnails and populates autofill properties - private static void SetUploadFile(IContentBase content, Property property, IFileSystem fs, string filepath, Stream filestream) - { - // check if file is an image (and supports resizing and thumbnails etc) - var extension = Path.GetExtension(filepath); - var isImage = ImageHelper.IsImageFile(extension); - - // specific stuff for images (thumbnails etc) - if (isImage) - { - using (var image = Image.FromStream(filestream)) - { - // use one image for all - ImageHelper.GenerateThumbnails(fs, image, filepath, property.PropertyType); - UploadAutoFillProperties.Populate(content, property.Alias, filepath, filestream, image); - } - } - else - { - // will use filepath for extension, and filestream for length - UploadAutoFillProperties.Populate(content, property.Alias, filepath, filestream); - } - } - } -} diff --git a/src/Umbraco.Core/Media/UploadAutoFillProperties.cs b/src/Umbraco.Core/Media/UploadAutoFillProperties.cs index 625bdc4e47..d0e7347848 100644 --- a/src/Umbraco.Core/Media/UploadAutoFillProperties.cs +++ b/src/Umbraco.Core/Media/UploadAutoFillProperties.cs @@ -2,7 +2,6 @@ using System.Drawing; using System.IO; using System.Linq; -using Umbraco.Core.Configuration; using Umbraco.Core.Configuration.UmbracoSettings; using Umbraco.Core.IO; using Umbraco.Core.Logging; @@ -13,16 +12,27 @@ namespace Umbraco.Core.Media /// /// Provides methods to manage auto-fill properties for upload fields. /// - internal static class UploadAutoFillProperties + internal class UploadAutoFillProperties { + private readonly ILogger _logger; + private readonly MediaFileSystem _mediaFileSystem; + private readonly IContentSection _contentSettings; + + public UploadAutoFillProperties(MediaFileSystem mediaFileSystem, ILogger logger, IContentSection contentSettings) + { + _mediaFileSystem = mediaFileSystem; + _logger = logger; + _contentSettings = contentSettings; + } + /// /// Gets the auto-fill configuration for a specified property alias. /// /// The property type alias. /// The auto-fill configuration for the specified property alias, or null. - public static IImagingAutoFillUploadField GetConfig(string propertyTypeAlias) + public IImagingAutoFillUploadField GetConfig(string propertyTypeAlias) { - var autoFillConfigs = UmbracoConfig.For.UmbracoSettings().Content.ImageAutoFillProperties; + var autoFillConfigs = _contentSettings.ImageAutoFillProperties; return autoFillConfigs == null ? null : autoFillConfigs.FirstOrDefault(x => x.Alias == propertyTypeAlias); } @@ -31,7 +41,7 @@ namespace Umbraco.Core.Media /// /// The content item. /// The property type alias. - public static void Reset(IContentBase content, string propertyTypeAlias) + public void Reset(IContentBase content, string propertyTypeAlias) { if (content == null) throw new ArgumentNullException("content"); if (propertyTypeAlias == null) throw new ArgumentNullException("propertyTypeAlias"); @@ -49,7 +59,7 @@ namespace Umbraco.Core.Media /// /// The content item. /// The auto-fill configuration. - public static void Reset(IContentBase content, IImagingAutoFillUploadField autoFillConfig) + public void Reset(IContentBase content, IImagingAutoFillUploadField autoFillConfig) { if (content == null) throw new ArgumentNullException("content"); if (autoFillConfig == null) throw new ArgumentNullException("autoFillConfig"); @@ -63,7 +73,7 @@ namespace Umbraco.Core.Media /// The content item. /// The property type alias. /// The filesystem-relative filepath, or null to clear properties. - public static void Populate(IContentBase content, string propertyTypeAlias, string filepath) + public void Populate(IContentBase content, string propertyTypeAlias, string filepath) { if (content == null) throw new ArgumentNullException("content"); if (propertyTypeAlias == null) throw new ArgumentNullException("propertyTypeAlias"); @@ -87,7 +97,7 @@ namespace Umbraco.Core.Media /// The filesystem-relative filepath, or null to clear properties. /// The stream containing the file data. /// The file data as an image object. - public static void Populate(IContentBase content, string propertyTypeAlias, string filepath, Stream filestream, Image image = null) + public void Populate(IContentBase content, string propertyTypeAlias, string filepath, Stream filestream, Image image = null) { if (content == null) throw new ArgumentNullException("content"); if (propertyTypeAlias == null) throw new ArgumentNullException("propertyTypeAlias"); @@ -110,7 +120,7 @@ namespace Umbraco.Core.Media /// The auto-fill configuration. /// The filesystem path to the uploaded file. /// The parameter is the path relative to the filesystem. - public static void Populate(IContentBase content, IImagingAutoFillUploadField autoFillConfig, string filepath) + public void Populate(IContentBase content, IImagingAutoFillUploadField autoFillConfig, string filepath) { if (content == null) throw new ArgumentNullException("content"); if (autoFillConfig == null) throw new ArgumentNullException("autoFillConfig"); @@ -125,16 +135,16 @@ namespace Umbraco.Core.Media // if anything goes wrong, just reset the properties try { - using (var filestream = MediaHelper.FileSystem.OpenFile(filepath)) + using (var filestream = _mediaFileSystem.OpenFile(filepath)) { var extension = (Path.GetExtension(filepath) ?? "").TrimStart('.'); - var size = ImageHelper.IsImageFile(extension) ? (Size?)ImageHelper.GetDimensions(filestream) : null; + var size = _mediaFileSystem.IsImageFile(extension) ? (Size?) _mediaFileSystem.GetDimensions(filestream) : null; SetProperties(content, autoFillConfig, size, filestream.Length, extension); } } catch (Exception ex) { - LogHelper.Error(typeof(UploadAutoFillProperties), "Could not populate upload auto-fill properties for file \"" + _logger.Error(typeof(UploadAutoFillProperties), "Could not populate upload auto-fill properties for file \"" + filepath + "\".", ex); ResetProperties(content, autoFillConfig); } @@ -149,7 +159,7 @@ namespace Umbraco.Core.Media /// The filesystem-relative filepath, or null to clear properties. /// The stream containing the file data. /// The file data as an image object. - public static void Populate(IContentBase content, IImagingAutoFillUploadField autoFillConfig, string filepath, Stream filestream, Image image = null) + public void Populate(IContentBase content, IImagingAutoFillUploadField autoFillConfig, string filepath, Stream filestream, Image image = null) { if (content == null) throw new ArgumentNullException("content"); if (autoFillConfig == null) throw new ArgumentNullException("autoFillConfig"); @@ -164,7 +174,7 @@ namespace Umbraco.Core.Media var extension = (Path.GetExtension(filepath) ?? "").TrimStart('.'); Size? size; if (image == null) - size = ImageHelper.IsImageFile(extension) ? (Size?) ImageHelper.GetDimensions(filestream) : null; + size = _mediaFileSystem.IsImageFile(extension) ? (Size?) _mediaFileSystem.GetDimensions(filestream) : null; else size = new Size(image.Width, image.Height); SetProperties(content, autoFillConfig, size, filestream.Length, extension); diff --git a/src/Umbraco.Core/Models/ContentExtensions.cs b/src/Umbraco.Core/Models/ContentExtensions.cs index e54653ea29..49a6e89226 100644 --- a/src/Umbraco.Core/Models/ContentExtensions.cs +++ b/src/Umbraco.Core/Models/ContentExtensions.cs @@ -474,7 +474,7 @@ namespace Umbraco.Core.Models if (string.IsNullOrWhiteSpace(filename)) return; filename = filename.ToLower(); // fixme - er... why? - MediaHelper.SetUploadFile(content, propertyTypeAlias, filename, value.InputStream); + FileSystemProviderManager.Current.MediaFileSystem.SetUploadFile(content, propertyTypeAlias, filename, value.InputStream); } /// @@ -519,7 +519,7 @@ namespace Umbraco.Core.Models if (string.IsNullOrWhiteSpace(filename)) return; filename = filename.ToLower(); // fixme - er... why? - MediaHelper.SetUploadFile(content, propertyTypeAlias, filename, filestream); + FileSystemProviderManager.Current.MediaFileSystem.SetUploadFile(content, propertyTypeAlias, filename, filestream); } /// @@ -543,7 +543,7 @@ namespace Umbraco.Core.Models var propertyType = content.GetContentType() .CompositionPropertyTypes.FirstOrDefault(x => x.Alias.InvariantEquals(propertyTypeAlias)); if (propertyType == null) throw new ArgumentException("Invalid property type alias " + propertyTypeAlias + "."); - return MediaHelper.StoreFile(content, propertyType, filename, filestream, filepath); + return FileSystemProviderManager.Current.MediaFileSystem.StoreFile(content, propertyType, filename, filestream, filepath); } #endregion diff --git a/src/Umbraco.Core/Models/PublicAccessEntry.cs b/src/Umbraco.Core/Models/PublicAccessEntry.cs index 4af6f9536a..969e853f05 100644 --- a/src/Umbraco.Core/Models/PublicAccessEntry.cs +++ b/src/Umbraco.Core/Models/PublicAccessEntry.cs @@ -107,10 +107,7 @@ namespace Umbraco.Core.Models public void ClearRules() { - for (var i = _ruleCollection.Count - 1; i >= 0; i--) - { - RemoveRule(_ruleCollection[i]); - } + _ruleCollection.Clear(); } [DataMember] diff --git a/src/Umbraco.Core/Packaging/PackageBinaryInspector.cs b/src/Umbraco.Core/Packaging/PackageBinaryInspector.cs index 6f048bbf2c..57664b8a83 100644 --- a/src/Umbraco.Core/Packaging/PackageBinaryInspector.cs +++ b/src/Umbraco.Core/Packaging/PackageBinaryInspector.cs @@ -82,7 +82,7 @@ namespace Umbraco.Core.Packaging /// /// Performs the assembly scanning /// - /// + /// /// /// /// @@ -111,7 +111,7 @@ namespace Umbraco.Core.Packaging /// /// Performs the assembly scanning /// - /// + /// /// /// /// @@ -158,7 +158,7 @@ namespace Umbraco.Core.Packaging //get the list of assembly names to compare below var loadedNames = loaded.Select(x => x.GetName().Name).ToArray(); - + //Then load each referenced assembly into the context foreach (var a in loaded) { @@ -174,7 +174,7 @@ namespace Umbraco.Core.Packaging } catch (FileNotFoundException) { - //if an exception occurs it means that a referenced assembly could not be found + //if an exception occurs it means that a referenced assembly could not be found errors.Add( string.Concat("This package references the assembly '", assemblyName.Name, @@ -183,7 +183,7 @@ namespace Umbraco.Core.Packaging } catch (Exception ex) { - //if an exception occurs it means that a referenced assembly could not be found + //if an exception occurs it means that a referenced assembly could not be found errors.Add( string.Concat("This package could not be verified for compatibility. An error occurred while loading a referenced assembly '", assemblyName.Name, @@ -201,7 +201,7 @@ namespace Umbraco.Core.Packaging { //now we need to see if they contain any type 'T' var reflectedAssembly = a; - + try { var found = reflectedAssembly.GetExportedTypes() @@ -214,8 +214,8 @@ namespace Umbraco.Core.Packaging } catch (Exception ex) { - //This is a hack that nobody can seem to get around, I've read everything and it seems that - // this is quite a common thing when loading types into reflection only load context, so + //This is a hack that nobody can seem to get around, I've read everything and it seems that + // this is quite a common thing when loading types into reflection only load context, so // we're just going to ignore this specific one for now var typeLoadEx = ex as TypeLoadException; if (typeLoadEx != null) @@ -236,7 +236,7 @@ namespace Umbraco.Core.Packaging LogHelper.Error("An error occurred scanning package assemblies", ex); } } - + } errorReport = errors.ToArray(); @@ -256,7 +256,7 @@ namespace Umbraco.Core.Packaging var contractType = contractAssemblyLoadFrom.GetExportedTypes() .FirstOrDefault(x => x.FullName == typeof(T).FullName && x.Assembly.FullName == typeof(T).Assembly.FullName); - + if (contractType == null) { throw new InvalidOperationException("Could not find type " + typeof(T) + " in the LoadFrom assemblies"); diff --git a/src/Umbraco.Core/Persistence/DefaultDatabaseFactory.cs b/src/Umbraco.Core/Persistence/DefaultDatabaseFactory.cs index a9c7a976a3..781d496a33 100644 --- a/src/Umbraco.Core/Persistence/DefaultDatabaseFactory.cs +++ b/src/Umbraco.Core/Persistence/DefaultDatabaseFactory.cs @@ -19,7 +19,7 @@ namespace Umbraco.Core.Persistence private readonly ILogger _logger; public string ConnectionString { get; private set; } public string ProviderName { get; private set; } - + // NO! see notes in v8 HybridAccessorBase //[ThreadStatic] //private static volatile UmbracoDatabase _nonHttpInstance; diff --git a/src/Umbraco.Core/Persistence/Repositories/VersionableRepositoryBase.cs b/src/Umbraco.Core/Persistence/Repositories/VersionableRepositoryBase.cs index 4d48652c0b..ded026bbec 100644 --- a/src/Umbraco.Core/Persistence/Repositories/VersionableRepositoryBase.cs +++ b/src/Umbraco.Core/Persistence/Repositories/VersionableRepositoryBase.cs @@ -657,7 +657,7 @@ WHERE EXISTS( var allsuccess = true; - var fs = FileSystemProviderManager.Current.GetFileSystemProvider(); + var fs = FileSystemProviderManager.Current.MediaFileSystem; Parallel.ForEach(files, file => { try @@ -677,7 +677,7 @@ WHERE EXISTS( } else { - ImageHelper.DeleteFile(fs, file, true); + fs.DeleteFile(file, true); } } catch (Exception e) diff --git a/src/Umbraco.Core/Services/MediaService.cs b/src/Umbraco.Core/Services/MediaService.cs index 8a86e59cc3..b9026eb6c4 100644 --- a/src/Umbraco.Core/Services/MediaService.cs +++ b/src/Umbraco.Core/Services/MediaService.cs @@ -8,6 +8,7 @@ using System.Text.RegularExpressions; using System.Threading; using Umbraco.Core.Configuration; using Umbraco.Core.Events; +using Umbraco.Core.IO; using Umbraco.Core.Logging; using Umbraco.Core.Media; using Umbraco.Core.Models; @@ -33,6 +34,7 @@ namespace Umbraco.Core.Services private readonly EntityXmlSerializer _entitySerializer = new EntityXmlSerializer(); private readonly IDataTypeService _dataTypeService; private readonly IUserService _userService; + private readonly MediaFileSystem _mediaFileSystem = FileSystemProviderManager.Current.MediaFileSystem; public MediaService(IDatabaseUnitOfWorkProvider provider, RepositoryFactory repositoryFactory, ILogger logger, IEventMessagesFactory eventMessagesFactory, IDataTypeService dataTypeService, IUserService userService) : base(provider, repositoryFactory, logger, eventMessagesFactory) @@ -1295,11 +1297,11 @@ namespace Umbraco.Core.Services public Stream GetMediaFileContentStream(string filepath) { - if (MediaHelper.FileSystem.FileExists(filepath) == false) + if (_mediaFileSystem.FileExists(filepath) == false) return null; try { - return MediaHelper.FileSystem.OpenFile(filepath); + return _mediaFileSystem.OpenFile(filepath); } catch { @@ -1309,19 +1311,19 @@ namespace Umbraco.Core.Services public void SetMediaFileContent(string filepath, Stream stream) { - MediaHelper.FileSystem.AddFile(filepath, stream, true); + _mediaFileSystem.AddFile(filepath, stream, true); } public void DeleteMediaFile(string filepath) { - ImageHelper.DeleteFile(MediaHelper.FileSystem, filepath, true); + _mediaFileSystem.DeleteFile(filepath, true); } public void GenerateThumbnails(string filepath, PropertyType propertyType) { - using (var filestream = MediaHelper.FileSystem.OpenFile(filepath)) + using (var filestream = _mediaFileSystem.OpenFile(filepath)) { - ImageHelper.GenerateThumbnails(MediaHelper.FileSystem, filestream, filepath, propertyType); + _mediaFileSystem.GenerateThumbnails(filestream, filepath, propertyType); } } diff --git a/src/Umbraco.Core/Umbraco.Core.csproj b/src/Umbraco.Core/Umbraco.Core.csproj index 6715f89c29..c9b86c4c8e 100644 --- a/src/Umbraco.Core/Umbraco.Core.csproj +++ b/src/Umbraco.Core/Umbraco.Core.csproj @@ -146,6 +146,7 @@ + @@ -356,6 +357,7 @@ + @@ -371,7 +373,7 @@ - + @@ -581,7 +583,6 @@ - diff --git a/src/Umbraco.Tests/IO/ShadowFileSystemTests.cs b/src/Umbraco.Tests/IO/ShadowFileSystemTests.cs index 94ce25cc98..540070df31 100644 --- a/src/Umbraco.Tests/IO/ShadowFileSystemTests.cs +++ b/src/Umbraco.Tests/IO/ShadowFileSystemTests.cs @@ -6,12 +6,18 @@ using System.Threading; using NUnit.Framework; using Umbraco.Core; using Umbraco.Core.IO; +using Umbraco.Tests.TestHelpers; namespace Umbraco.Tests.IO { [TestFixture] public class ShadowFileSystemTests { + // tested: + // only 1 instance of this class is created + // SetUp and TearDown run before/after each test + // SetUp does not start before the previous TearDown returns + [SetUp] public void SetUp() { @@ -28,20 +34,8 @@ namespace Umbraco.Tests.IO private static void ClearFiles() { - var path = IOHelper.MapPath("FileSysTests"); - if (Directory.Exists(path)) - { - foreach (var file in Directory.EnumerateFiles(path, "*", SearchOption.AllDirectories)) - File.Delete(file); - Directory.Delete(path, true); - } - path = IOHelper.MapPath("App_Data"); - if (Directory.Exists(path)) - { - foreach (var file in Directory.EnumerateFiles(path, "*", SearchOption.AllDirectories)) - File.Delete(file); - Directory.Delete(path, true); - } + TestHelper.DeleteDirectory(IOHelper.MapPath("FileSysTests")); + TestHelper.DeleteDirectory(IOHelper.MapPath("App_Data")); } private static string NormPath(string path) @@ -341,8 +335,7 @@ namespace Umbraco.Tests.IO Assert.IsTrue(File.Exists(path + "/ShadowSystem/path/to/some/dir/f1.txt")); - // kill everything and let the shadow fs die - Directory.Delete(path + "/ShadowSystem", true); + // let the shadow fs die } [Test] @@ -420,7 +413,7 @@ namespace Umbraco.Tests.IO Assert.AreEqual(1, Directory.GetDirectories(appdata + "/Shadow").Length); scope.Complete(); Assert.IsTrue(fs.FileExists("sub/f4.txt")); - Assert.AreEqual(0, Directory.GetDirectories(appdata + "/Shadow").Length); + TestHelper.TryAssert(() => Assert.AreEqual(0, Directory.GetDirectories(appdata + "/Shadow").Length)); scope.Dispose(); Assert.IsTrue(fs.FileExists("sub/f4.txt")); Assert.IsFalse(Directory.Exists(appdata + "/Shadow/" + id)); @@ -477,7 +470,7 @@ namespace Umbraco.Tests.IO Assert.IsTrue(fs.FileExists("sub/f2.txt")); scope.Dispose(); Assert.IsTrue(fs.FileExists("sub/f2.txt")); - Assert.IsFalse(Directory.Exists(appdata + "/Shadow/" + id)); + TestHelper.TryAssert(() => Assert.IsFalse(Directory.Exists(appdata + "/Shadow/" + id))); string text; using (var s = fs.OpenFile("sub/f2.txt")) @@ -580,9 +573,8 @@ namespace Umbraco.Tests.IO File.WriteAllText(path + "/test/inner/f3.txt", "foo"); path = NormPath(path); - Directory.Delete(path, true); - - Assert.IsFalse(File.Exists(path + "/test/inner/f3.txt")); + TestHelper.Try(() => Directory.Delete(path, true)); + TestHelper.TryAssert(() => Assert.IsFalse(File.Exists(path + "/test/inner/f3.txt"))); } } } diff --git a/src/Umbraco.Tests/TestHelpers/TestHelper.cs b/src/Umbraco.Tests/TestHelpers/TestHelper.cs index 6c8fc9308d..de62bff271 100644 --- a/src/Umbraco.Tests/TestHelpers/TestHelper.cs +++ b/src/Umbraco.Tests/TestHelpers/TestHelper.cs @@ -6,6 +6,7 @@ using System.Configuration; using System.IO; using System.Linq; using System.Reflection; +using System.Threading; using NUnit.Framework; using SqlCE4Umbraco; using Umbraco.Core; @@ -203,6 +204,50 @@ namespace Umbraco.Tests.TestHelpers } } + public static void DeleteDirectory(string path) + { + Try(() => + { + if (Directory.Exists(path) == false) return; + foreach (var file in Directory.EnumerateFiles(path, "*", SearchOption.AllDirectories)) + File.Delete(file); + }); + Try(() => + { + if (Directory.Exists(path) == false) return; + Directory.Delete(path, true); + }); + } + + public static void TryAssert(Action action, int maxTries = 5, int waitMilliseconds = 200) + { + Try(action, maxTries, waitMilliseconds); + } + + public static void Try(Action action, int maxTries = 5, int waitMilliseconds = 200) + { + Try(action, maxTries, waitMilliseconds); + } + + public static void Try(Action action, int maxTries = 5, int waitMilliseconds = 200) + where T : Exception + { + var tries = 0; + while (true) + { + try + { + action(); + break; + } + catch (T) + { + if (tries++ > maxTries) + throw; + Thread.Sleep(waitMilliseconds); + } + } + } } } \ No newline at end of file diff --git a/src/Umbraco.Web/Editors/ImagesController.cs b/src/Umbraco.Web/Editors/ImagesController.cs index eae620e745..fa84ff345f 100644 --- a/src/Umbraco.Web/Editors/ImagesController.cs +++ b/src/Umbraco.Web/Editors/ImagesController.cs @@ -104,11 +104,11 @@ namespace Umbraco.Web.Editors /// private HttpResponseMessage GetResized(string imagePath, int width, string sizeName) { - var fs = FileSystemProviderManager.Current.GetFileSystemProvider(); + var fs = FileSystemProviderManager.Current.MediaFileSystem; var ext = Path.GetExtension(imagePath); // we need to check if it is an image by extension - if (ImageHelper.IsImageFile(ext) == false) + if (fs.IsImageFile(ext) == false) return Request.CreateResponse(HttpStatusCode.NotFound); //redirect to ImageProcessor thumbnail with rnd generated from last modified time of original media file diff --git a/src/Umbraco.Web/PropertyEditors/FileUploadPropertyEditor.cs b/src/Umbraco.Web/PropertyEditors/FileUploadPropertyEditor.cs index 616c814731..7cef6fc1cc 100644 --- a/src/Umbraco.Web/PropertyEditors/FileUploadPropertyEditor.cs +++ b/src/Umbraco.Web/PropertyEditors/FileUploadPropertyEditor.cs @@ -4,7 +4,7 @@ using System.ComponentModel.DataAnnotations; using System.Linq; using Newtonsoft.Json.Linq; using Umbraco.Core; -using Umbraco.Core.Media; +using Umbraco.Core.IO; using Umbraco.Core.Models; using Umbraco.Core.PropertyEditors; using Umbraco.Core.Services; @@ -14,6 +14,12 @@ namespace Umbraco.Web.PropertyEditors [PropertyEditor(Constants.PropertyEditors.UploadFieldAlias, "File upload", "fileupload", Icon = "icon-download-alt", Group = "media")] public class FileUploadPropertyEditor : PropertyEditor, IApplicationEventHandler { + private static MediaFileSystem MediaFileSystem + { + // v8 will get rid of singletons + get { return FileSystemProviderManager.Current.MediaFileSystem; } + } + /// /// Creates the corresponding property value editor. /// @@ -22,7 +28,7 @@ namespace Umbraco.Web.PropertyEditors { var baseEditor = base.CreateValueEditor(); baseEditor.Validators.Add(new UploadFileTypeValidator()); - return new FileUploadPropertyValueEditor(baseEditor); + return new FileUploadPropertyValueEditor(baseEditor, MediaFileSystem); } /// @@ -55,11 +61,9 @@ namespace Umbraco.Web.PropertyEditors /// The properties that were deleted. static IEnumerable GetFilesToDelete(IEnumerable properties) { - var fs = MediaHelper.FileSystem; - return properties .Where(x => IsUploadField(x, true)) - .Select(x => fs.GetRelativePath((string) x.Value)) + .Select(x => MediaFileSystem.GetRelativePath((string) x.Value)) .ToList(); } @@ -75,12 +79,11 @@ namespace Umbraco.Web.PropertyEditors // copy files var isUpdated = false; - var fs = MediaHelper.FileSystem; foreach (var property in properties) { - var sourcePath = fs.GetRelativePath((string) property.Value); - var copyPath = MediaHelper.CopyFile(args.Copy, property.PropertyType, sourcePath); - args.Copy.SetValue(property.Alias, fs.GetUrl(copyPath)); + var sourcePath = MediaFileSystem.GetRelativePath((string) property.Value); + var copyPath = MediaFileSystem.CopyFile(args.Copy, property.PropertyType, sourcePath); + args.Copy.SetValue(property.Alias, MediaFileSystem.GetUrl(copyPath)); isUpdated = true; } @@ -128,18 +131,17 @@ namespace Umbraco.Web.PropertyEditors static void AutoFillProperties(IContentBase content) { var properties = content.Properties.Where(x => IsUploadField(x, false)); - var fs = MediaHelper.FileSystem; foreach (var property in properties) { - var autoFillConfig = UploadAutoFillProperties.GetConfig(property.Alias); + var autoFillConfig = MediaFileSystem.UploadAutoFillProperties.GetConfig(property.Alias); if (autoFillConfig == null) continue; var svalue = property.Value as string; if (string.IsNullOrWhiteSpace(svalue)) - UploadAutoFillProperties.Reset(content, autoFillConfig); + MediaFileSystem.UploadAutoFillProperties.Reset(content, autoFillConfig); else - UploadAutoFillProperties.Populate(content, autoFillConfig, fs.GetRelativePath(svalue)); + MediaFileSystem.UploadAutoFillProperties.Populate(content, autoFillConfig, MediaFileSystem.GetRelativePath(svalue)); } } diff --git a/src/Umbraco.Web/PropertyEditors/FileUploadPropertyValueEditor.cs b/src/Umbraco.Web/PropertyEditors/FileUploadPropertyValueEditor.cs index 1bba10c7a3..777a14b768 100644 --- a/src/Umbraco.Web/PropertyEditors/FileUploadPropertyValueEditor.cs +++ b/src/Umbraco.Web/PropertyEditors/FileUploadPropertyValueEditor.cs @@ -1,21 +1,13 @@ using System; using System.Collections.Generic; using System.Drawing; -using System.Globalization; using System.IO; using System.Linq; -using Newtonsoft.Json; using Newtonsoft.Json.Linq; -using Umbraco.Core.Configuration; using Umbraco.Core.IO; -using Umbraco.Core.Logging; -using Umbraco.Core.Media; using Umbraco.Core.Models.Editors; using Umbraco.Core.PropertyEditors; using Umbraco.Web.Models.ContentEditing; -using umbraco; -using umbraco.cms.businesslogic.Files; -using Umbraco.Core; namespace Umbraco.Web.PropertyEditors { @@ -24,9 +16,13 @@ namespace Umbraco.Web.PropertyEditors /// internal class FileUploadPropertyValueEditor : PropertyValueEditorWrapper { - public FileUploadPropertyValueEditor(PropertyValueEditor wrapped) + private readonly MediaFileSystem _mediaFileSystem; + + public FileUploadPropertyValueEditor(PropertyValueEditor wrapped, MediaFileSystem mediaFileSystem) : base(wrapped) - { } + { + _mediaFileSystem = mediaFileSystem; + } /// /// Converts the value received from the editor into the value can be stored in the database. @@ -65,17 +61,16 @@ namespace Umbraco.Web.PropertyEditors return currentValue; // get the current file paths - var fs = MediaHelper.FileSystem; var currentPaths = currentValue.ToString() .Split(new[] { ',' }, StringSplitOptions.RemoveEmptyEntries) - .Select(x => fs.GetRelativePath(x)) // get the fs-relative path + .Select(x => _mediaFileSystem.GetRelativePath(x)) // get the fs-relative path .ToArray(); // if clearing, remove these files and return if (clears) { foreach (var pathToRemove in currentPaths) - ImageHelper.DeleteFile(fs, pathToRemove, true); + _mediaFileSystem.DeleteFile(pathToRemove, true); return string.Empty; // no more files } @@ -108,19 +103,19 @@ namespace Umbraco.Web.PropertyEditors // get the filepath // in case we are using the old path scheme, try to re-use numbers (bah...) var reuse = i < currentPaths.Length ? currentPaths[i] : null; // this would be WRONG with many files - var filepath = MediaHelper.GetMediaPath(file.FileName, reuse, cuid, puid); // fs-relative path + var filepath = _mediaFileSystem.GetMediaPath(file.FileName, reuse, cuid, puid); // fs-relative path using (var filestream = File.OpenRead(file.TempFilePath)) { - fs.AddFile(filepath, filestream, true); // must overwrite! + _mediaFileSystem.AddFile(filepath, filestream, true); // must overwrite! - var ext = fs.GetExtension(filepath); - if (ImageHelper.IsImageFile(ext)) + var ext = _mediaFileSystem.GetExtension(filepath); + if (_mediaFileSystem.IsImageFile(ext)) { var preValues = editorValue.PreValues.FormatAsDictionary(); var sizes = preValues.Any() ? preValues.First().Value.Value : string.Empty; using (var image = Image.FromStream(filestream)) - ImageHelper.GenerateThumbnails(fs, image, filepath, sizes); + _mediaFileSystem.GenerateThumbnails(image, filepath, sizes); } // all related properties (auto-fill) are managed by FileUploadPropertyEditor @@ -136,10 +131,10 @@ namespace Umbraco.Web.PropertyEditors // remove files that are not there anymore foreach (var pathToRemove in currentPaths.Except(newPaths)) - ImageHelper.DeleteFile(fs, pathToRemove, true); + _mediaFileSystem.DeleteFile(pathToRemove, true); - return string.Join(",", newPaths.Select(x => fs.GetUrl(x))); + return string.Join(",", newPaths.Select(x => _mediaFileSystem.GetUrl(x))); } } } \ No newline at end of file diff --git a/src/Umbraco.Web/PropertyEditors/ImageCropperPropertyEditor.cs b/src/Umbraco.Web/PropertyEditors/ImageCropperPropertyEditor.cs index 9b9974c705..a7669b0c9c 100644 --- a/src/Umbraco.Web/PropertyEditors/ImageCropperPropertyEditor.cs +++ b/src/Umbraco.Web/PropertyEditors/ImageCropperPropertyEditor.cs @@ -4,6 +4,7 @@ using System; using System.Collections.Generic; using System.Linq; using Umbraco.Core; +using Umbraco.Core.IO; using Umbraco.Core.Logging; using Umbraco.Core.Media; using Umbraco.Core.Models; @@ -36,6 +37,12 @@ namespace Umbraco.Web.PropertyEditors }; } + private static MediaFileSystem MediaFileSystem + { + // v8 will get rid of singletons + get { return FileSystemProviderManager.Current.MediaFileSystem; } + } + /// /// Creates the corresponding property value editor. /// @@ -43,7 +50,7 @@ namespace Umbraco.Web.PropertyEditors protected override PropertyValueEditor CreateValueEditor() { var baseEditor = base.CreateValueEditor(); - return new ImageCropperPropertyValueEditor(baseEditor); + return new ImageCropperPropertyValueEditor(baseEditor, MediaFileSystem); } /// @@ -100,14 +107,12 @@ namespace Umbraco.Web.PropertyEditors /// The properties that were deleted. static IEnumerable GetFilesToDelete(IEnumerable properties) { - var fs = MediaHelper.FileSystem; - return properties.Where(x => IsCropperField(x, true)).Select(x => { var jo = GetJObject((string) x.Value, true); if (jo == null || jo["src"] == null) return null; var src = jo["src"].Value(); - return string.IsNullOrWhiteSpace(src) ? null : fs.GetRelativePath(src); + return string.IsNullOrWhiteSpace(src) ? null : MediaFileSystem.GetRelativePath(src); }).WhereNotNull(); } @@ -123,7 +128,6 @@ namespace Umbraco.Web.PropertyEditors // copy files var isUpdated = false; - var fs = MediaHelper.FileSystem; foreach (var property in properties) { var jo = GetJObject((string) property.Value, true); @@ -132,9 +136,9 @@ namespace Umbraco.Web.PropertyEditors var src = jo["src"].Value(); if (string.IsNullOrWhiteSpace(src)) continue; - var sourcePath = fs.GetRelativePath(src); - var copyPath = MediaHelper.CopyFile(args.Copy, property.PropertyType, sourcePath); - jo["src"] = fs.GetUrl(copyPath); + var sourcePath = MediaFileSystem.GetRelativePath(src); + var copyPath = MediaFileSystem.CopyFile(args.Copy, property.PropertyType, sourcePath); + jo["src"] = MediaFileSystem.GetUrl(copyPath); args.Copy.SetValue(property.Alias, jo.ToString()); isUpdated = true; } @@ -183,17 +187,16 @@ namespace Umbraco.Web.PropertyEditors static void AutoFillProperties(IContentBase content) { var properties = content.Properties.Where(x => IsCropperField(x, false)); - var fs = MediaHelper.FileSystem; foreach (var property in properties) { - var autoFillConfig = UploadAutoFillProperties.GetConfig(property.Alias); + var autoFillConfig = MediaFileSystem.UploadAutoFillProperties.GetConfig(property.Alias); if (autoFillConfig == null) continue; var svalue = property.Value as string; if (string.IsNullOrWhiteSpace(svalue)) { - UploadAutoFillProperties.Reset(content, autoFillConfig); + MediaFileSystem.UploadAutoFillProperties.Reset(content, autoFillConfig); continue; } @@ -218,9 +221,9 @@ namespace Umbraco.Web.PropertyEditors } if (src == null) - UploadAutoFillProperties.Reset(content, autoFillConfig); + MediaFileSystem.UploadAutoFillProperties.Reset(content, autoFillConfig); else - UploadAutoFillProperties.Populate(content, autoFillConfig, fs.GetRelativePath(src)); + MediaFileSystem.UploadAutoFillProperties.Populate(content, autoFillConfig, MediaFileSystem.GetRelativePath(src)); } } diff --git a/src/Umbraco.Web/PropertyEditors/ImageCropperPropertyValueEditor.cs b/src/Umbraco.Web/PropertyEditors/ImageCropperPropertyValueEditor.cs index f1f26f003b..f717711bac 100644 --- a/src/Umbraco.Web/PropertyEditors/ImageCropperPropertyValueEditor.cs +++ b/src/Umbraco.Web/PropertyEditors/ImageCropperPropertyValueEditor.cs @@ -28,8 +28,13 @@ namespace Umbraco.Web.PropertyEditors /// internal class ImageCropperPropertyValueEditor : PropertyValueEditorWrapper { - public ImageCropperPropertyValueEditor(PropertyValueEditor wrapped) : base(wrapped) - { } + private MediaFileSystem _mediaFileSystem; + + public ImageCropperPropertyValueEditor(PropertyValueEditor wrapped, MediaFileSystem mediaFileSystem) + : base(wrapped) + { + _mediaFileSystem = mediaFileSystem; + } /// /// This is called to merge in the prevalue crops with the value that is saved - similar to the property value converter for the front-end @@ -65,8 +70,6 @@ namespace Umbraco.Web.PropertyEditors /// public override object ConvertEditorToDb(ContentPropertyData editorValue, object currentValue) { - var fs = MediaHelper.FileSystem; - // get the current path var currentPath = string.Empty; try @@ -82,7 +85,7 @@ namespace Umbraco.Web.PropertyEditors LogHelper.WarnWithException("Could not parse current db value to a JObject.", ex); } if (string.IsNullOrWhiteSpace(currentPath) == false) - currentPath = fs.GetRelativePath(currentPath); + currentPath = _mediaFileSystem.GetRelativePath(currentPath); // get the new json and path JObject editorJson = null; @@ -122,7 +125,7 @@ namespace Umbraco.Web.PropertyEditors // value is unchanged. if (string.IsNullOrWhiteSpace(editorFile) && string.IsNullOrWhiteSpace(currentPath) == false) { - ImageHelper.DeleteFile(fs, currentPath, true); + _mediaFileSystem.DeleteFile(currentPath, true); return null; // clear } @@ -130,7 +133,7 @@ namespace Umbraco.Web.PropertyEditors } // process the file - var filepath = editorJson == null ? null : ProcessFile(editorValue, file, fs, currentPath, cuid, puid); + var filepath = editorJson == null ? null : ProcessFile(editorValue, file, currentPath, cuid, puid); // remove all temp files foreach (var f in files) @@ -138,15 +141,15 @@ namespace Umbraco.Web.PropertyEditors // remove current file if replaced if (currentPath != filepath && string.IsNullOrWhiteSpace(currentPath) == false) - ImageHelper.DeleteFile(fs, currentPath, true); + _mediaFileSystem.DeleteFile(currentPath, true); // update json and return if (editorJson == null) return null; - editorJson["src"] = filepath == null ? string.Empty : fs.GetUrl(filepath); + editorJson["src"] = filepath == null ? string.Empty : _mediaFileSystem.GetUrl(filepath); return editorJson.ToString(); } - private string ProcessFile(ContentPropertyData editorValue, ContentItemFile file, IFileSystem fs, string currentPath, Guid cuid, Guid puid) + private string ProcessFile(ContentPropertyData editorValue, ContentItemFile file, string currentPath, Guid cuid, Guid puid) { // process the file // no file, invalid file, reject change @@ -155,19 +158,19 @@ namespace Umbraco.Web.PropertyEditors // get the filepath // in case we are using the old path scheme, try to re-use numbers (bah...) - var filepath = MediaHelper.GetMediaPath(file.FileName, currentPath, cuid, puid); // fs-relative path + var filepath = _mediaFileSystem.GetMediaPath(file.FileName, currentPath, cuid, puid); // fs-relative path using (var filestream = File.OpenRead(file.TempFilePath)) { - fs.AddFile(filepath, filestream, true); // must overwrite! + _mediaFileSystem.AddFile(filepath, filestream, true); // must overwrite! - var ext = fs.GetExtension(filepath); - if (ImageHelper.IsImageFile(ext)) + var ext = _mediaFileSystem.GetExtension(filepath); + if (_mediaFileSystem.IsImageFile(ext)) { var preValues = editorValue.PreValues.FormatAsDictionary(); var sizes = preValues.Any() ? preValues.First().Value.Value : string.Empty; using (var image = Image.FromStream(filestream)) - ImageHelper.GenerateThumbnails(fs, image, filepath, sizes); + _mediaFileSystem.GenerateThumbnails(image, filepath, sizes); } // all related properties (auto-fill) are managed by ImageCropperPropertyEditor @@ -194,6 +197,4 @@ namespace Umbraco.Web.PropertyEditors return newVal; } } - - } diff --git a/src/umbraco.cms/businesslogic/Content.cs b/src/umbraco.cms/businesslogic/Content.cs index e7b9d7fe66..c55204fe8e 100644 --- a/src/umbraco.cms/businesslogic/Content.cs +++ b/src/umbraco.cms/businesslogic/Content.cs @@ -632,7 +632,7 @@ namespace umbraco.cms.businesslogic } else { - ImageHelper.DeleteFile(fs, relativeFilePath, true); + fs.DeleteFile(relativeFilePath, true); } } } diff --git a/src/umbraco.cms/businesslogic/datatype/FileHandlerData.cs b/src/umbraco.cms/businesslogic/datatype/FileHandlerData.cs index b80b2892a4..5eecd607b5 100644 --- a/src/umbraco.cms/businesslogic/datatype/FileHandlerData.cs +++ b/src/umbraco.cms/businesslogic/datatype/FileHandlerData.cs @@ -73,7 +73,7 @@ namespace umbraco.cms.businesslogic.datatype int subfolderId; var numberedFolder = int.TryParse(subfolder, out subfolderId) ? subfolderId.ToString(CultureInfo.InvariantCulture) - : MediaHelper.GetNextFolder(); + : fs.GetNextFolder(); var fileName = UmbracoConfig.For.UmbracoSettings().Content.UploadAllowDirectories ? Path.Combine(numberedFolder, name)