diff --git a/src/Orchard.Azure.Tests/App.config b/src/Orchard.Azure.Tests/App.config
new file mode 100644
index 000000000..c244681d2
--- /dev/null
+++ b/src/Orchard.Azure.Tests/App.config
@@ -0,0 +1,6 @@
+
+
+
+
+
+
\ No newline at end of file
diff --git a/src/Orchard.Azure.Tests/Orchard.Azure.Tests.csproj b/src/Orchard.Azure.Tests/Orchard.Azure.Tests.csproj
new file mode 100644
index 000000000..231b57883
--- /dev/null
+++ b/src/Orchard.Azure.Tests/Orchard.Azure.Tests.csproj
@@ -0,0 +1,80 @@
+
+
+
+ Debug
+ AnyCPU
+ 9.0.30729
+ 2.0
+ {1CC62F45-E6FF-43D5-84BF-509A1085D994}
+ Library
+ Properties
+ Orchard.Azure.Tests
+ Orchard.Azure.Tests
+ v3.5
+ 512
+
+
+ true
+ full
+ false
+ bin\Debug\
+ DEBUG;TRACE
+ prompt
+ 4
+
+
+ pdbonly
+ true
+ bin\Release\
+ TRACE
+ prompt
+ 4
+
+
+
+
+
+
+ False
+ ..\..\lib\nunit\nunit.framework.dll
+
+
+
+
+ 3.5
+
+
+ 3.5
+
+
+ 3.5
+
+
+
+
+
+
+
+
+
+
+ {2505AA84-65A6-43D0-9C27-4F44FD576284}
+ Orchard.Azure
+
+
+ {2D1D92BB-4555-4CBE-8D0E-63563D6CE4C6}
+ Orchard.Framework
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/src/Orchard.Azure.Tests/Properties/AssemblyInfo.cs b/src/Orchard.Azure.Tests/Properties/AssemblyInfo.cs
new file mode 100644
index 000000000..8c0fd184d
--- /dev/null
+++ b/src/Orchard.Azure.Tests/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("Orchard.Azure.Tests")]
+[assembly: AssemblyDescription("")]
+[assembly: AssemblyConfiguration("")]
+[assembly: AssemblyCompany("Microsoft")]
+[assembly: AssemblyProduct("Orchard.Azure.Tests")]
+[assembly: AssemblyCopyright("Copyright © Microsoft 2010")]
+[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("21e6aa35-d13d-495b-af35-bc066a1a8bf2")]
+
+// 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/Orchard.Azure.Tests/Storage/AzureBlobStorageProviderTests.cs b/src/Orchard.Azure.Tests/Storage/AzureBlobStorageProviderTests.cs
new file mode 100644
index 000000000..c9979df6b
--- /dev/null
+++ b/src/Orchard.Azure.Tests/Storage/AzureBlobStorageProviderTests.cs
@@ -0,0 +1,211 @@
+using System;
+using System.Configuration;
+using System.IO;
+using NUnit.Framework;
+using System.Diagnostics;
+using Orchard.Azure.Storage;
+using Microsoft.WindowsAzure;
+using System.Linq;
+using Microsoft.WindowsAzure.StorageClient;
+using System.Text;
+
+namespace Orchard.Azure.Tests.Storage {
+ [TestFixture]
+ public class AzureBlobStorageProviderTests {
+
+ #region Azure Environment initialization
+
+ private Process _dsService;
+
+ [TestFixtureSetUp]
+ public void FixtureSetup() {
+ var count = Process.GetProcessesByName("DSService").Length;
+ if (count == 0)
+ {
+ var start = new ProcessStartInfo
+ {
+ Arguments = "/devstore:start",
+ FileName =
+ Path.Combine(ConfigurationManager.AppSettings["AzureSDK"], @"bin\csrun.exe")
+ };
+
+ _dsService = new Process { StartInfo = start };
+ _dsService.Start();
+ _dsService.WaitForExit();
+ }
+
+ CloudStorageAccount devAccount;
+ CloudStorageAccount.TryParse("UseDevelopmentStorage=true", out devAccount);
+
+ _azureBlobStorageProvider = new AzureBlobStorageProvider("default", devAccount);
+ }
+
+ [TestFixtureTearDown]
+ public void FixtureTearDown() {
+
+ if(_dsService != null)
+ _dsService.Close();
+ }
+
+ [SetUp]
+ public void Setup() {
+ // ensure default container is empty before running any test
+ DeleteAllBlobs();
+ }
+
+ #endregion
+
+ private AzureBlobStorageProvider _azureBlobStorageProvider;
+
+ private void DeleteAllBlobs() {
+ foreach(var blob in _azureBlobStorageProvider.Container.ListBlobs()) {
+ if(blob is CloudBlob) {
+ ((CloudBlob) blob).DeleteIfExists();
+ }
+
+ if (blob is CloudBlobDirectory) {
+ DeleteAllBlobs((CloudBlobDirectory)blob);
+ }
+ }
+ }
+
+ private static void DeleteAllBlobs(CloudBlobDirectory cloudBlobDirectory) {
+ foreach (var blob in cloudBlobDirectory.ListBlobs()) {
+ if (blob is CloudBlob) {
+ ((CloudBlob)blob).DeleteIfExists();
+ }
+
+ if (blob is CloudBlobDirectory) {
+ DeleteAllBlobs((CloudBlobDirectory)blob);
+ }
+ }
+ }
+
+ [Test]
+ [ExpectedException(typeof(ArgumentException))]
+ public void GetFileShouldOnlyAcceptRelativePath() {
+ _azureBlobStorageProvider.CreateFile("foo.txt");
+ _azureBlobStorageProvider.GetFile("/foot.txt");
+ }
+
+ [Test]
+ [ExpectedException(typeof(ArgumentException))]
+ public void GetFileThatDoesNotExistShouldThrow() {
+ _azureBlobStorageProvider.GetFile("notexisting");
+ }
+
+ [Test]
+ [ExpectedException(typeof(ArgumentException))]
+ public void DeleteFileThatDoesNotExistShouldThrow() {
+ _azureBlobStorageProvider.DeleteFile("notexisting");
+ }
+
+ [Test]
+ public void CreateFileShouldReturnCorrectStorageFile() {
+ var storageFile = _azureBlobStorageProvider.CreateFile("foo.txt");
+
+ Assert.AreEqual(".txt", storageFile.GetFileType());
+ Assert.AreEqual("foo.txt", storageFile.GetName());
+ Assert.That(storageFile.GetPath().EndsWith("/default/foo.txt"), Is.True);
+ Assert.AreEqual(0, storageFile.GetSize());
+ }
+
+ [Test]
+ [ExpectedException(typeof(ArgumentException))]
+ public void CreateFileShouldThrowAnExceptionIfAlreadyExisting() {
+ var storageFile = _azureBlobStorageProvider.CreateFile("foo.txt");
+ Assert.AreEqual(storageFile.GetSize(), 0);
+
+ _azureBlobStorageProvider.CreateFile("foo.txt");
+ }
+
+ [Test]
+ public void RenameFileShouldCreateANewFileAndRemoveTheOld() {
+ _azureBlobStorageProvider.CreateFile("foo1.txt");
+ _azureBlobStorageProvider.RenameFile("foo1.txt", "foo2.txt");
+
+ var files = _azureBlobStorageProvider.ListFiles("");
+
+ Assert.AreEqual(1, files.Count());
+ Assert.That(files.First().GetPath().EndsWith("foo2.txt"), Is.True);
+ }
+
+ [Test]
+ public void CreateFileShouldBeFolderAgnostic()
+ {
+ _azureBlobStorageProvider.CreateFile("foo.txt");
+ _azureBlobStorageProvider.CreateFile("folder/foo.txt");
+ _azureBlobStorageProvider.CreateFile("folder/folder/foo.txt");
+
+ Assert.AreEqual(1, _azureBlobStorageProvider.ListFiles("").Count());
+ Assert.AreEqual(1, _azureBlobStorageProvider.ListFiles("folder").Count());
+ Assert.AreEqual(1, _azureBlobStorageProvider.ListFiles("folder/folder").Count());
+ }
+
+ [Test]
+ [ExpectedException(typeof(ArgumentException))]
+ public void CreateFolderThatExistsShouldThrow() {
+ // sebros: In Azure, the folder concept is just about checking files prefix. So until a file exists, a folder is nothing
+ _azureBlobStorageProvider.CreateFile("folder/foo.txt");
+ _azureBlobStorageProvider.CreateFolder("folder");
+ }
+
+ [Test]
+ public void DeleteFolderShouldDeleteFilesAlso() {
+ _azureBlobStorageProvider.CreateFile("folder/foo1.txt");
+ _azureBlobStorageProvider.CreateFile("folder/foo2.txt");
+ _azureBlobStorageProvider.CreateFile("folder/folder/foo1.txt");
+ _azureBlobStorageProvider.CreateFile("folder/folder/foo2.txt");
+
+ Assert.AreEqual(2, _azureBlobStorageProvider.ListFiles("folder").Count());
+ Assert.AreEqual(2, _azureBlobStorageProvider.ListFiles("folder/folder").Count());
+
+ _azureBlobStorageProvider.DeleteFolder("folder");
+
+ Assert.AreEqual(0, _azureBlobStorageProvider.ListFiles("folder").Count());
+ Assert.AreEqual(0, _azureBlobStorageProvider.ListFiles("folder/folder").Count());
+ }
+
+ [Test]
+ public void ShouldRenameFolders() {
+ _azureBlobStorageProvider.CreateFile("folder1/foo.txt");
+ _azureBlobStorageProvider.CreateFile("folder2/foo.txt");
+ _azureBlobStorageProvider.CreateFile("folder1/folder2/foo.txt");
+ _azureBlobStorageProvider.CreateFile("folder1/folder2/folder3/foo.txt");
+
+ Assert.AreEqual(1, _azureBlobStorageProvider.ListFiles("folder1").Count());
+ Assert.AreEqual(1, _azureBlobStorageProvider.ListFiles("folder2").Count());
+ Assert.AreEqual(1, _azureBlobStorageProvider.ListFiles("folder1/folder2").Count());
+ Assert.AreEqual(1, _azureBlobStorageProvider.ListFiles("folder1/folder2/folder3").Count());
+
+ _azureBlobStorageProvider.RenameFolder("folder1/folder2", "folder1/folder4");
+
+ Assert.AreEqual(1, _azureBlobStorageProvider.ListFiles("folder1").Count());
+ Assert.AreEqual(1, _azureBlobStorageProvider.ListFiles("folder2").Count());
+ Assert.AreEqual(0, _azureBlobStorageProvider.ListFiles("folder1/folder2").Count());
+ Assert.AreEqual(1, _azureBlobStorageProvider.ListFiles("folder1/folder4").Count());
+ Assert.AreEqual(1, _azureBlobStorageProvider.ListFiles("folder1/folder4/folder3").Count());
+ }
+
+ [Test]
+ public void ShouldReadWriteFiles() {
+ const string teststring = "This is a test string.";
+
+ var foo = _azureBlobStorageProvider.CreateFile("folder1/foo.txt");
+
+ using(var stream = foo.OpenWrite())
+ using (var writer = new StreamWriter(stream))
+ writer.Write(teststring);
+
+ Assert.AreEqual(22, foo.GetSize());
+
+ string content;
+ using ( var stream = foo.OpenRead() )
+ using ( var reader = new StreamReader(stream) ) {
+ content = reader.ReadToEnd();
+ }
+
+ Assert.AreEqual(teststring, content);
+ }
+ }
+}
diff --git a/src/Orchard.Azure.sln b/src/Orchard.Azure.sln
new file mode 100644
index 000000000..6e952e846
--- /dev/null
+++ b/src/Orchard.Azure.sln
@@ -0,0 +1,32 @@
+
+Microsoft Visual Studio Solution File, Format Version 10.00
+# Visual Studio 2008
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Orchard.Azure", "Orchard.Azure\Orchard.Azure.csproj", "{2505AA84-65A6-43D0-9C27-4F44FD576284}"
+EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Orchard.Azure.Tests", "Orchard.Azure.Tests\Orchard.Azure.Tests.csproj", "{1CC62F45-E6FF-43D5-84BF-509A1085D994}"
+EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Orchard.Framework", "Orchard\Orchard.Framework.csproj", "{2D1D92BB-4555-4CBE-8D0E-63563D6CE4C6}"
+EndProject
+Global
+ GlobalSection(SolutionConfigurationPlatforms) = preSolution
+ Debug|Any CPU = Debug|Any CPU
+ Release|Any CPU = Release|Any CPU
+ EndGlobalSection
+ GlobalSection(ProjectConfigurationPlatforms) = postSolution
+ {2505AA84-65A6-43D0-9C27-4F44FD576284}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {2505AA84-65A6-43D0-9C27-4F44FD576284}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {2505AA84-65A6-43D0-9C27-4F44FD576284}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {2505AA84-65A6-43D0-9C27-4F44FD576284}.Release|Any CPU.Build.0 = Release|Any CPU
+ {1CC62F45-E6FF-43D5-84BF-509A1085D994}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {1CC62F45-E6FF-43D5-84BF-509A1085D994}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {1CC62F45-E6FF-43D5-84BF-509A1085D994}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {1CC62F45-E6FF-43D5-84BF-509A1085D994}.Release|Any CPU.Build.0 = Release|Any CPU
+ {2D1D92BB-4555-4CBE-8D0E-63563D6CE4C6}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {2D1D92BB-4555-4CBE-8D0E-63563D6CE4C6}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {2D1D92BB-4555-4CBE-8D0E-63563D6CE4C6}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {2D1D92BB-4555-4CBE-8D0E-63563D6CE4C6}.Release|Any CPU.Build.0 = Release|Any CPU
+ EndGlobalSection
+ GlobalSection(SolutionProperties) = preSolution
+ HideSolutionNode = FALSE
+ EndGlobalSection
+EndGlobal
diff --git a/src/Orchard.Azure/Orchard.Azure.csproj b/src/Orchard.Azure/Orchard.Azure.csproj
new file mode 100644
index 000000000..69c7f0ec3
--- /dev/null
+++ b/src/Orchard.Azure/Orchard.Azure.csproj
@@ -0,0 +1,66 @@
+
+
+
+ Debug
+ AnyCPU
+ 9.0.30729
+ 2.0
+ {2505AA84-65A6-43D0-9C27-4F44FD576284}
+ Library
+ Properties
+ Orchard.Azure
+ Orchard.Azure
+ v3.5
+ 512
+
+
+ true
+ full
+ false
+ bin\Debug\
+ DEBUG;TRACE
+ prompt
+ 4
+
+
+ pdbonly
+ true
+ bin\Release\
+ TRACE
+ prompt
+ 4
+
+
+
+
+
+ 3.5
+
+
+ 3.5
+
+
+ 3.5
+
+
+
+
+
+
+
+
+
+
+ {2D1D92BB-4555-4CBE-8D0E-63563D6CE4C6}
+ Orchard.Framework
+
+
+
+
+
\ No newline at end of file
diff --git a/src/Orchard.Azure/Properties/AssemblyInfo.cs b/src/Orchard.Azure/Properties/AssemblyInfo.cs
new file mode 100644
index 000000000..17cd66b92
--- /dev/null
+++ b/src/Orchard.Azure/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("Orchard.Azure")]
+[assembly: AssemblyDescription("")]
+[assembly: AssemblyConfiguration("")]
+[assembly: AssemblyCompany("Microsoft")]
+[assembly: AssemblyProduct("Orchard.Azure")]
+[assembly: AssemblyCopyright("Copyright © Microsoft 2010")]
+[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("02bb8033-01ce-4c2d-a3f7-2a03641f4640")]
+
+// 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/Orchard.Azure/Storage/AzureBlobStorageProvider.cs b/src/Orchard.Azure/Storage/AzureBlobStorageProvider.cs
new file mode 100644
index 000000000..b79fecd88
--- /dev/null
+++ b/src/Orchard.Azure/Storage/AzureBlobStorageProvider.cs
@@ -0,0 +1,317 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using Microsoft.WindowsAzure;
+using Microsoft.WindowsAzure.StorageClient;
+using System.IO;
+using Orchard.Storage;
+
+namespace Orchard.Azure.Storage
+{
+ public interface IDependency {}
+
+ public class AzureBlobStorageProvider : IStorageProvider
+ {
+ private readonly CloudStorageAccount _storageAccount;
+ public CloudBlobClient BlobClient { get; private set; }
+ public CloudBlobContainer Container { get; private set; }
+
+ public AzureBlobStorageProvider(string containerName) : this(containerName, CloudStorageAccount.FromConfigurationSetting("DataConnectionString"))
+ {
+ }
+
+ public AzureBlobStorageProvider(string containerName, CloudStorageAccount storageAccount)
+ {
+ // Setup the connection to custom storage accountm, e.g. Development Storage
+ _storageAccount = storageAccount;
+
+ BlobClient = _storageAccount.CreateCloudBlobClient();
+ // Get and create the container if it does not exist
+ // The container is named with DNS naming restrictions (i.e. all lower case)
+ Container = BlobClient.GetContainerReference(containerName);
+
+ Container.SetPermissions(new BlobContainerPermissions { PublicAccess = BlobContainerPublicAccessType.Container });
+ }
+
+ private static void EnsurePathIsRelative(string path) {
+ if(path.StartsWith("/"))
+ throw new ArgumentException("Path must be relative");
+ }
+
+ private string GetPrefix(string path) {
+ var prefix = String.Concat(Container.Name, "/", path);
+ if (prefix.EndsWith("/"))
+ return prefix;
+
+ return String.Concat(prefix, "/");
+ }
+
+ private bool BlobExists(string path) {
+ if(String.IsNullOrEmpty(path) || path.Trim() == String.Empty)
+ throw new ArgumentException("Path can't be empty");
+
+ try {
+ var blob = Container.GetBlockBlobReference(path);
+ blob.FetchAttributes();
+ return true;
+ }
+ catch (StorageClientException e) {
+ if (e.ErrorCode == StorageErrorCode.ResourceNotFound) {
+ return false;
+ }
+
+ throw;
+ }
+ }
+
+ private void EnsureBlobExists(string path)
+ {
+ if (!BlobExists(path)) {
+ throw new ArgumentException("File " + path + " does not exist");
+ }
+ }
+
+ private void EnsureBlobDoesNotExist(string path)
+ {
+ if (BlobExists(path)) {
+ throw new ArgumentException("File " + path + " already exists");
+ }
+ }
+
+ private bool DirectoryExists(string path)
+ {
+ if (String.IsNullOrEmpty(path) || path.Trim() == String.Empty)
+ throw new ArgumentException("Path can't be empty");
+
+ return Container.GetDirectoryReference(path).ListBlobs().Count() > 0;
+ }
+
+ private void EnsureDirectoryExists(string path)
+ {
+ if (!DirectoryExists(path)) {
+ throw new ArgumentException("Directory " + path + " does not exist");
+ }
+ }
+
+ private void EnsureDirectoryDoesNotExist(string path)
+ {
+ if (DirectoryExists(path)) {
+ throw new ArgumentException("Directory " + path + " already exists");
+ }
+ }
+
+ #region IStorageProvider Members
+
+ public IStorageFile GetFile(string path) {
+ EnsurePathIsRelative(path);
+ EnsureBlobExists(path);
+ return new AzureBlobFileStorage(Container.GetBlockBlobReference(path));
+ }
+
+ public IEnumerable ListFiles(string path)
+ {
+ EnsurePathIsRelative(path);
+ foreach (var blobItem in BlobClient.ListBlobsWithPrefix(GetPrefix(path)).OfType()) {
+ yield return new AzureBlobFileStorage(blobItem);
+ }
+ }
+
+ public IEnumerable ListFolders(string path)
+ {
+ EnsurePathIsRelative(path);
+ if (!DirectoryExists(path))
+ {
+ try {
+ CreateFolder(path);
+ }
+ catch (Exception ex) {
+ throw new ArgumentException(string.Format("The folder could not be created at path: {0}. {1}", path, ex));
+ }
+ }
+
+ return Container.GetDirectoryReference(path)
+ .ListBlobs()
+ .OfType()
+ .Select(d => new AzureBlobFolderStorage(d))
+ .ToList();
+ }
+
+ public void CreateFolder(string path) {
+ EnsurePathIsRelative(path);
+ EnsureDirectoryDoesNotExist(path);
+ Container.GetDirectoryReference(path);
+ }
+
+ public void DeleteFolder(string path) {
+ EnsureDirectoryExists(path);
+ foreach (var blob in Container.GetDirectoryReference(path).ListBlobs()) {
+ if (blob is CloudBlob)
+ ((CloudBlob)blob).Delete();
+
+ if (blob is CloudBlobDirectory)
+ DeleteFolder(blob.Uri.ToString());
+ }
+ }
+
+ public void RenameFolder(string path, string newPath) {
+ EnsurePathIsRelative(path);
+ EnsurePathIsRelative(newPath);
+
+ if ( !path.EndsWith("/") )
+ path += "/";
+
+ if ( !newPath.EndsWith("/") )
+ newPath += "/";
+
+ foreach ( var blob in Container.GetDirectoryReference(path).ListBlobs() ) {
+ if ( blob is CloudBlob ) {
+ string filename = Path.GetFileName(blob.Uri.ToString());
+ string source = String.Concat(path, filename);
+ string destination = String.Concat(newPath, filename);
+ RenameFile(source, destination);
+ }
+
+ if ( blob is CloudBlobDirectory ) {
+ string foldername = blob.Uri.Segments.Last();
+ string source = String.Concat(path, foldername);
+ string destination = String.Concat(newPath, foldername);
+ RenameFolder(source, destination);
+ }
+ }
+
+ }
+
+ public void DeleteFile(string path) {
+ EnsurePathIsRelative(path);
+ EnsureBlobExists(path);
+ var blob = Container.GetBlockBlobReference(path);
+ blob.Delete();
+ }
+
+ public void RenameFile(string path, string newPath) {
+ EnsurePathIsRelative(path);
+ EnsurePathIsRelative(newPath);
+ EnsureBlobExists(path);
+ EnsureBlobDoesNotExist(newPath);
+
+ var blob = Container.GetBlockBlobReference(path);
+ var newBlob = Container.GetBlockBlobReference(newPath);
+ newBlob.CopyFromBlob(blob);
+ blob.Delete();
+ }
+
+ public IStorageFile CreateFile(string path) {
+ EnsurePathIsRelative(path);
+ if (BlobExists(path)) {
+ throw new ArgumentException("File " + path + " already exists");
+ }
+
+ var blob = Container.GetBlockBlobReference(path);
+ blob.OpenWrite().Dispose(); // force file creation
+ return new AzureBlobFileStorage(blob);
+ }
+
+ public string Combine(string path1, string path2) {
+ EnsurePathIsRelative(path1);
+ EnsurePathIsRelative(path2);
+
+ if (path1 == null || path2 == null)
+ throw new ArgumentException("One or more path is null");
+
+ if (path1.Trim() == String.Empty)
+ return path2;
+
+ if (path2.Trim() == String.Empty)
+ return path1;
+
+ var uri1 = new Uri(path1);
+ var uri2 = new Uri(path2);
+
+ return uri2.IsAbsoluteUri ? uri2.ToString() : new Uri(uri1, uri2).ToString();
+ }
+
+ #endregion
+
+ private class AzureBlobFileStorage : IStorageFile {
+ private readonly CloudBlockBlob _blob;
+
+ public AzureBlobFileStorage(CloudBlockBlob blob) {
+ _blob = blob;
+ }
+
+ public string GetPath() {
+ return _blob.Uri.ToString();
+ }
+
+ public string GetName() {
+ return Path.GetFileName(GetPath());
+ }
+
+ public long GetSize() {
+ return _blob.Properties.Length;
+ }
+
+ public DateTime GetLastUpdated() {
+ return _blob.Properties.LastModifiedUtc;
+ }
+
+ public string GetFileType() {
+ return Path.GetExtension(GetPath());
+ }
+
+ public Stream OpenRead() {
+ return _blob.OpenRead();
+ }
+
+ public Stream OpenWrite() {
+ return _blob.OpenWrite();
+ }
+
+ }
+
+ private class AzureBlobFolderStorage : IStorageFolder {
+ private readonly CloudBlobDirectory _blob;
+
+ public AzureBlobFolderStorage(CloudBlobDirectory blob) {
+ _blob = blob;
+ }
+
+ #region IStorageFolder Members
+
+ public string GetName() {
+ return _blob.Uri.ToString();
+ }
+
+ public long GetSize() {
+ return GetDirectorySize(_blob);
+ }
+
+ public DateTime GetLastUpdated() {
+ return DateTime.MinValue;
+ }
+
+ public IStorageFolder GetParent() {
+ if (_blob.Parent != null) {
+ return new AzureBlobFolderStorage(_blob.Parent);
+ }
+ throw new ArgumentException("Directory " + _blob.Uri + " does not have a parent directory");
+ }
+
+ private static long GetDirectorySize(CloudBlobDirectory directoryBlob) {
+ long size = 0;
+
+ foreach (var blobItem in directoryBlob.ListBlobs()) {
+ if (blobItem is CloudBlob)
+ size += ((CloudBlob)blobItem).Properties.Length;
+
+ if (blobItem is CloudBlobDirectory)
+ size += GetDirectorySize((CloudBlobDirectory)blobItem);
+ }
+
+ return size;
+ }
+
+ #endregion
+ }
+ }
+}
diff --git a/src/Orchard.Specs/Bindings/CommandLine.cs b/src/Orchard.Specs/Bindings/CommandLine.cs
new file mode 100644
index 000000000..50d0e361e
--- /dev/null
+++ b/src/Orchard.Specs/Bindings/CommandLine.cs
@@ -0,0 +1,34 @@
+using System;
+using System.IO;
+using System.Linq;
+using Orchard.Commands;
+using Orchard.Parameters;
+using Orchard.Specs.Hosting;
+using TechTalk.SpecFlow;
+
+namespace Orchard.Specs.Bindings {
+ [Binding]
+ public class CommandLine : BindingBase {
+ [When(@"I execute >(.*)")]
+ public void WhenIExecute(string commandLine) {
+ var details = new RequestDetails();
+ Binding().Host.Execute(() => {
+ var args = commandLine.Split(new[] { ' ' }, StringSplitOptions.RemoveEmptyEntries);
+ var parameters = new CommandParametersParser().Parse(args);
+ var agent = new CommandHostAgent();
+ var input = new StringReader("");
+ var output = new StringWriter();
+ details.StatusCode = agent.RunSingleCommand(
+ input,
+ output,
+ "Default",
+ parameters.Arguments.ToArray(),
+ parameters.Switches.ToDictionary(kv => kv.Key, kv => kv.Value));
+ details.StatusDescription = details.StatusCode.ToString();
+ details.ResponseText = output.ToString();
+ });
+
+ Binding().Details = details;
+ }
+ }
+}
diff --git a/src/Orchard.Specs/Bindings/OrchardSiteFactory.cs b/src/Orchard.Specs/Bindings/OrchardSiteFactory.cs
index 423c8980a..605692f79 100644
--- a/src/Orchard.Specs/Bindings/OrchardSiteFactory.cs
+++ b/src/Orchard.Specs/Bindings/OrchardSiteFactory.cs
@@ -47,7 +47,7 @@ namespace Orchard.Specs.Bindings {
descriptor.EnabledFeatures.Concat(new[] { new ShellFeature { Name = name } }),
descriptor.Parameters);
}
-
+
Trace.WriteLine("This call to Host.Reinitialize should not be needed, eventually");
MvcApplication.Host.Reinitialize_Obsolete();
});
@@ -62,7 +62,30 @@ namespace Orchard.Specs.Bindings {
Trace.WriteLine("This call to Host.Reinitialize should not be needed, eventually");
MvcApplication.Host.Reinitialize_Obsolete();
});
+ }
+ [Given(@"I have tenant ""(.*)\"" on ""(.*)\"" as ""(.*)\""")]
+ public void GivenIHaveTenantOnSiteAsName(string shellName, string hostName, string siteName) {
+ var webApp = Binding();
+ webApp.Host.Execute(() => {
+ var shellSettings = new ShellSettings {
+ Name = shellName,
+ RequestUrlHost = hostName,
+ State = new TenantState("Uninitialized"),
+ };
+ using (var environment = MvcApplication.CreateStandaloneEnvironment("Default")) {
+ environment.Resolve().SaveSettings(shellSettings);
+ }
+ MvcApplication.Host.Reinitialize_Obsolete();
+ });
+
+ webApp.WhenIGoToPathOnHost("Setup", hostName);
+
+ webApp.WhenIFillIn(TableData(
+ new { name = "SiteName", value = siteName },
+ new { name = "AdminPassword", value = "6655321" }));
+
+ webApp.WhenIHit("Finish Setup");
}
}
}
diff --git a/src/Orchard.Specs/Bindings/WebAppHosting.cs b/src/Orchard.Specs/Bindings/WebAppHosting.cs
index 6b6fa82a5..99e439472 100644
--- a/src/Orchard.Specs/Bindings/WebAppHosting.cs
+++ b/src/Orchard.Specs/Bindings/WebAppHosting.cs
@@ -30,6 +30,11 @@ namespace Orchard.Specs.Bindings {
get { return _webHost; }
}
+ public RequestDetails Details {
+ get { return _details; }
+ set { _details = value; }
+ }
+
[Given(@"I have a clean site")]
public void GivenIHaveACleanSite() {
GivenIHaveACleanSiteBasedOn("Orchard.Web");
@@ -122,16 +127,16 @@ namespace Orchard.Specs.Bindings {
[When(@"I go to ""(.*)"" on host (.*)")]
public void WhenIGoToPathOnHost(string urlPath, string host) {
Host.HostName = host;
- _details = Host.SendRequest(urlPath);
+ Details = Host.SendRequest(urlPath);
_doc = new HtmlDocument();
- _doc.Load(new StringReader(_details.ResponseText));
+ _doc.Load(new StringReader(Details.ResponseText));
}
[When(@"I go to ""(.*)""")]
public void WhenIGoTo(string urlPath) {
- _details = Host.SendRequest(urlPath);
+ Details = Host.SendRequest(urlPath);
_doc = new HtmlDocument();
- _doc.Load(new StringReader(_details.ResponseText));
+ _doc.Load(new StringReader(Details.ResponseText));
}
[When(@"I follow ""(.*)""")]
@@ -153,7 +158,7 @@ namespace Orchard.Specs.Bindings {
foreach (var row in table.Rows) {
var r = row;
var input = inputs.SingleOrDefault(x => x.GetAttributeValue("name", x.GetAttributeValue("id", "")) == r["name"]);
- Assert.That(input, Is.Not.Null, "Unable to locate name {0} in page html:\r\n\r\n{1}", r["name"], _details.ResponseText);
+ Assert.That(input, Is.Not.Null, "Unable to locate name {0} in page html:\r\n\r\n{1}", r["name"], Details.ResponseText);
input.Attributes.Add("value", row["value"]);
}
}
@@ -165,21 +170,21 @@ namespace Orchard.Specs.Bindings {
.Single(elt => elt.GetAttributeValue("value", null) == submitText);
var form = Form.LocateAround(submit);
- var urlPath = form.Start.GetAttributeValue("action", _details.UrlPath);
+ var urlPath = form.Start.GetAttributeValue("action", Details.UrlPath);
var inputs = form.Children
.SelectMany(elt => elt.DescendantsAndSelf("input"))
.GroupBy(elt => elt.GetAttributeValue("name", elt.GetAttributeValue("id", "")), elt => elt.GetAttributeValue("value", ""))
.ToDictionary(elt => elt.Key, elt => (IEnumerable)elt);
- _details = Host.SendRequest(urlPath, inputs);
+ Details = Host.SendRequest(urlPath, inputs);
_doc = new HtmlDocument();
- _doc.Load(new StringReader(_details.ResponseText));
+ _doc.Load(new StringReader(Details.ResponseText));
}
[When(@"I am redirected")]
public void WhenIAmRedirected() {
var urlPath = "";
- if (_details.ResponseHeaders.TryGetValue("Location", out urlPath)) {
+ if (Details.ResponseHeaders.TryGetValue("Location", out urlPath)) {
WhenIGoTo(urlPath);
}
else {
@@ -189,13 +194,13 @@ namespace Orchard.Specs.Bindings {
[Then(@"the status should be (.*) (.*)")]
public void ThenTheStatusShouldBe(int statusCode, string statusDescription) {
- Assert.That(_details.StatusCode, Is.EqualTo(statusCode));
- Assert.That(_details.StatusDescription, Is.EqualTo(statusDescription));
+ Assert.That(Details.StatusCode, Is.EqualTo(statusCode));
+ Assert.That(Details.StatusDescription, Is.EqualTo(statusDescription));
}
[Then(@"I should see ""(.*)""")]
public void ThenIShouldSee(string text) {
- Assert.That(_details.ResponseText, Is.StringContaining(text));
+ Assert.That(Details.ResponseText, Is.StringContaining(text));
}
[Then(@"the title contains ""(.*)""")]
diff --git a/src/Orchard.Specs/Hosting/Orchard.Web/Config/Diagnostics.config b/src/Orchard.Specs/Hosting/Orchard.Web/Config/Diagnostics.config
index f65fef8c7..a44c108d4 100644
--- a/src/Orchard.Specs/Hosting/Orchard.Web/Config/Diagnostics.config
+++ b/src/Orchard.Specs/Hosting/Orchard.Web/Config/Diagnostics.config
@@ -26,9 +26,6 @@
-
-
diff --git a/src/Orchard.Specs/Hosting/Orchard.Web/Config/Sites.Default.config b/src/Orchard.Specs/Hosting/Orchard.Web/Config/Sites.config
similarity index 100%
rename from src/Orchard.Specs/Hosting/Orchard.Web/Config/Sites.Default.config
rename to src/Orchard.Specs/Hosting/Orchard.Web/Config/Sites.config
diff --git a/src/Orchard.Specs/Hosting/TraceEnabledSessionFactoryBuilder.cs b/src/Orchard.Specs/Hosting/TraceEnabledSessionFactoryBuilder.cs
index f05e8a392..cb82f295a 100644
--- a/src/Orchard.Specs/Hosting/TraceEnabledSessionFactoryBuilder.cs
+++ b/src/Orchard.Specs/Hosting/TraceEnabledSessionFactoryBuilder.cs
@@ -17,7 +17,8 @@ namespace Orchard.Specs.Hosting {
}
protected override IPersistenceConfigurer GetPersistenceConfigurer(bool createDatabase) {
var config = (SQLiteConfiguration)base.GetPersistenceConfigurer(createDatabase);
- return config.ShowSql();
+ //config.ShowSql();
+ return config;
}
}
}
diff --git a/src/Orchard.Specs/Hosting/WebHost.cs b/src/Orchard.Specs/Hosting/WebHost.cs
index c188f76e7..d834cbe01 100644
--- a/src/Orchard.Specs/Hosting/WebHost.cs
+++ b/src/Orchard.Specs/Hosting/WebHost.cs
@@ -28,6 +28,7 @@ namespace Orchard.Specs.Hosting {
baseDir
.ShallowCopy("*.dll", _tempSite.Combine("bin"))
+ .ShallowCopy("*.exe", _tempSite.Combine("bin"))
.ShallowCopy("*.pdb", _tempSite.Combine("bin"));
HostName = "localhost";
diff --git a/src/Orchard.Specs/MultiTenancy.feature b/src/Orchard.Specs/MultiTenancy.feature
index a32b0ab39..ed5bbed0b 100644
--- a/src/Orchard.Specs/MultiTenancy.feature
+++ b/src/Orchard.Specs/MultiTenancy.feature
@@ -74,3 +74,11 @@ Scenario: A new tenant runs the setup
And I go to "/Default.aspx"
Then I should see "Scott Site "
And I should see "Welcome, admin !"
+
+Scenario: Listing tenants from command line
+ Given I have installed Orchard
+ And I have installed "Orchard.MultiTenancy"
+ And I have tenant "Alpha" on "example.org" as "New-site-name"
+ When I execute >orchard tenant list
+ Then I should see "Name: Alpha"
+ And I should see "Request Url Host: example.org"
diff --git a/src/Orchard.Specs/MultiTenancy.feature.cs b/src/Orchard.Specs/MultiTenancy.feature.cs
index 4dd7fc195..701a66ee0 100644
--- a/src/Orchard.Specs/MultiTenancy.feature.cs
+++ b/src/Orchard.Specs/MultiTenancy.feature.cs
@@ -250,6 +250,29 @@ this.ScenarioSetup(scenarioInfo);
testRunner.Then("I should see \"Scott Site \"");
#line 76
testRunner.And("I should see \"Welcome, admin !\"");
+#line hidden
+ testRunner.CollectScenarioErrors();
+ }
+
+ [NUnit.Framework.TestAttribute()]
+ [NUnit.Framework.DescriptionAttribute("Listing tenants from command line")]
+ public virtual void ListingTenantsFromCommandLine()
+ {
+ TechTalk.SpecFlow.ScenarioInfo scenarioInfo = new TechTalk.SpecFlow.ScenarioInfo("Listing tenants from command line", ((string[])(null)));
+#line 78
+this.ScenarioSetup(scenarioInfo);
+#line 79
+ testRunner.Given("I have installed Orchard");
+#line 80
+ testRunner.And("I have installed \"Orchard.MultiTenancy\"");
+#line 81
+ testRunner.And("I have tenant \"Alpha\" on \"example.org\" as \"New-site-name\"");
+#line 82
+ testRunner.When("I execute >tenant list");
+#line 83
+ testRunner.Then("I should see \"Name: Alpha\"");
+#line 84
+ testRunner.And("I should see \"Request Url Host: example.org\"");
#line hidden
testRunner.CollectScenarioErrors();
}
diff --git a/src/Orchard.Specs/Orchard.Specs.csproj b/src/Orchard.Specs/Orchard.Specs.csproj
index 96cf0b66b..f6bfdc2d0 100644
--- a/src/Orchard.Specs/Orchard.Specs.csproj
+++ b/src/Orchard.Specs/Orchard.Specs.csproj
@@ -103,6 +103,7 @@
+
@@ -158,7 +159,7 @@
Always
-
+
Always
@@ -248,6 +249,10 @@
{2D1D92BB-4555-4CBE-8D0E-63563D6CE4C6}
Orchard.Framework
+
+ {33B1BC8D-E292-4972-A363-22056B207156}
+ Orchard
+
diff --git a/src/Orchard/Environment/IOrchardHost.cs b/src/Orchard/Environment/IOrchardHost.cs
index be434f7a7..314560677 100644
--- a/src/Orchard/Environment/IOrchardHost.cs
+++ b/src/Orchard/Environment/IOrchardHost.cs
@@ -28,4 +28,5 @@ namespace Orchard.Environment {
///
IStandaloneEnvironment CreateStandaloneEnvironment(ShellSettings shellSettings);
}
+
}
diff --git a/src/Orchard/Environment/ShellBuilders/ShellContainerFactory.cs b/src/Orchard/Environment/ShellBuilders/ShellContainerFactory.cs
index 0cf188489..2db70da88 100644
--- a/src/Orchard/Environment/ShellBuilders/ShellContainerFactory.cs
+++ b/src/Orchard/Environment/ShellBuilders/ShellContainerFactory.cs
@@ -87,9 +87,13 @@ namespace Orchard.Environment.ShellBuilders {
.InjectActionInvoker();
}
- var optionalHostConfig = HostingEnvironment.MapPath("~/Config/Sites." + settings.Name + ".config");
- if (File.Exists(optionalHostConfig))
- builder.RegisterModule(new ConfigurationSettingsReader(ConfigurationSettingsReader.DefaultSectionName, optionalHostConfig));
+ var optionalShellConfig = HostingEnvironment.MapPath("~/Config/Sites.config");
+ if (File.Exists(optionalShellConfig))
+ builder.RegisterModule(new ConfigurationSettingsReader(ConfigurationSettingsReader.DefaultSectionName, optionalShellConfig));
+
+ var optionalShellByNameConfig = HostingEnvironment.MapPath("~/Config/Sites." + settings.Name + ".config");
+ if (File.Exists(optionalShellByNameConfig))
+ builder.RegisterModule(new ConfigurationSettingsReader(ConfigurationSettingsReader.DefaultSectionName, optionalShellByNameConfig));
});
}
diff --git a/src/Orchard/Storage/FileSystemStorageProvider.cs b/src/Orchard/Storage/FileSystemStorageProvider.cs
index a2a8eeaed..f60f63031 100644
--- a/src/Orchard/Storage/FileSystemStorageProvider.cs
+++ b/src/Orchard/Storage/FileSystemStorageProvider.cs
@@ -95,7 +95,7 @@ namespace Orchard.Storage {
public void RenameFile(string path, string newPath) {
if (!File.Exists(path)) {
- throw new ArgumentException("File " + path + "does not exist");
+ throw new ArgumentException("File " + path + " does not exist");
}
if (File.Exists(newPath)) {
@@ -105,6 +105,11 @@ namespace Orchard.Storage {
File.Move(path, newPath);
}
+ public string Combine(string path1, string path2)
+ {
+ return Path.Combine(path1, path2);
+ }
+
#endregion
private class FileSystemStorageFile : IStorageFile {
@@ -136,8 +141,14 @@ namespace Orchard.Storage {
return _fileInfo.Extension;
}
- public Stream OpenStream() {
- return new FileStream(_fileInfo.FullName, FileMode.Open);
+ public Stream OpenRead()
+ {
+ return new FileStream(_fileInfo.FullName, FileMode.Open, FileAccess.Read);
+ }
+
+ public Stream OpenWrite()
+ {
+ return new FileStream(_fileInfo.FullName, FileMode.Open, FileAccess.ReadWrite);
}
#endregion
@@ -192,5 +203,6 @@ namespace Orchard.Storage {
return size;
}
}
+
}
}
diff --git a/src/Orchard/Storage/IStorageFile.cs b/src/Orchard/Storage/IStorageFile.cs
index bb47cd53d..bd7177bd4 100644
--- a/src/Orchard/Storage/IStorageFile.cs
+++ b/src/Orchard/Storage/IStorageFile.cs
@@ -8,6 +8,15 @@ namespace Orchard.Storage {
long GetSize();
DateTime GetLastUpdated();
string GetFileType();
- Stream OpenStream();
+
+ ///
+ /// Creates a stream for reading from the file.
+ ///
+ Stream OpenRead();
+
+ ///
+ /// Creates a stream for writing to the file.
+ ///
+ Stream OpenWrite();
}
}
diff --git a/src/Orchard/Storage/IStorageProvider.cs b/src/Orchard/Storage/IStorageProvider.cs
index 79b260556..25d4e3eea 100644
--- a/src/Orchard/Storage/IStorageProvider.cs
+++ b/src/Orchard/Storage/IStorageProvider.cs
@@ -11,5 +11,14 @@ namespace Orchard.Storage {
void DeleteFile(string path);
void RenameFile(string path, string newPath);
IStorageFile CreateFile(string path);
+
+ ///
+ /// Combines two path strings
+ ///
+ /// The first path
+ /// The second path
+ /// A string containing the combined paths. If one of the specified paths is a zero-length string, this method returns the other path.
+ /// If path2 contains an absolute path, this method returns path2.
+ string Combine(string path1, string path2);
}
}