Skip to content

Commit

Permalink
Fixed #271: Added zip controller to easily upload/download files
Browse files Browse the repository at this point in the history
  • Loading branch information
davidebbo committed Feb 11, 2013
1 parent 0264f93 commit 66d1a51
Show file tree
Hide file tree
Showing 10 changed files with 233 additions and 7 deletions.
1 change: 1 addition & 0 deletions Kudu.Client/Kudu.Client.csproj
Expand Up @@ -85,6 +85,7 @@
<Compile Include="SourceControl\RemoteRepository.cs" />
<Compile Include="SourceControl\RemoteRepositoryManager.cs" />
<Compile Include="SSHKey\RemoteSSHKeyManager.cs" />
<Compile Include="Zip\RemoteVfsManager.cs" />
</ItemGroup>
<ItemGroup>
<None Include="packages.config">
Expand Down
54 changes: 54 additions & 0 deletions Kudu.Client/Zip/RemoteVfsManager.cs
@@ -0,0 +1,54 @@
using System;
using System.IO;
using System.Net;
using System.Net.Http;
using System.Net.Http.Headers;
using Kudu.Client.Infrastructure;

namespace Kudu.Client.Editor
{
public class RemoteZipManager : KuduRemoteClientBase
{
public RemoteZipManager(string serviceUrl, ICredentials credentials = null, HttpMessageHandler handler = null)
: base(serviceUrl, credentials, handler)
{
}

public Stream GetZipStream(string path)
{
HttpResponseMessage response = Client.GetAsync(path).Result;
if (response.StatusCode == HttpStatusCode.NotFound)
{
return null;
}

return response
.EnsureSuccessful()
.Content
.ReadAsStreamAsync()
.Result;
}

public void PutZipStream(string path, Stream zipFile)
{
using (var request = new HttpRequestMessage())
{
request.Method = HttpMethod.Put;
request.RequestUri = new Uri(path, UriKind.Relative);
request.Headers.IfMatch.Add(EntityTagHeaderValue.Any);
request.Content = new StreamContent(zipFile);
Client.SendAsync(request).Result.EnsureSuccessful();
}
}


public void PutZipFile(string path, string localZipPath)
{
using (var stream = File.OpenRead(localZipPath))
{
PutZipStream(path, stream);
}
}
}
}

5 changes: 5 additions & 0 deletions Kudu.FunctionalTests/Kudu.FunctionalTests.csproj
Expand Up @@ -36,6 +36,10 @@
<Prefer32Bit>false</Prefer32Bit>
</PropertyGroup>
<ItemGroup>
<Reference Include="Ionic.Zip, Version=1.9.1.8, Culture=neutral, PublicKeyToken=edbe51ad942a3f5c, processorArchitecture=MSIL">
<SpecificVersion>False</SpecificVersion>
<HintPath>..\packages\DotNetZip.1.9.1.8\lib\net20\Ionic.Zip.dll</HintPath>
</Reference>
<Reference Include="Microsoft.Web.Administration, Version=7.9.0.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35, processorArchitecture=MSIL">
<SpecificVersion>False</SpecificVersion>
<HintPath>..\..\..\..\..\..\..\Windows\System32\inetsrv\Microsoft.Web.Administration.dll</HintPath>
Expand Down Expand Up @@ -79,6 +83,7 @@
</ItemGroup>
<ItemGroup>
<Compile Include="CommandExecutorTests.cs" />
<Compile Include="ZipTests.cs" />
<Compile Include="DeploymentManagerTests.cs" />
<Compile Include="DiagnosticsApiFacts.cs" />
<Compile Include="DropboxTests.cs" />
Expand Down
58 changes: 58 additions & 0 deletions Kudu.FunctionalTests/ZipTests.cs
@@ -0,0 +1,58 @@
using System.IO;
using Ionic.Zip;
using Kudu.TestHarness;
using Xunit;

namespace Kudu.FunctionalTests
{
public class ZipTests
{
[Fact]
public void TestZipController()
{
ApplicationManager.Run("TestZip", appManager =>
{
string tempDirectory = Path.Combine(Path.GetTempPath(), Path.GetRandomFileName());
Directory.CreateDirectory(tempDirectory);
string tempZipPath = Path.GetTempFileName();
try
{
// Create file on server using vfs
appManager.VfsWebRootManager.WriteAllText("foo.txt", "Hello zip");
// Make sure it's part of the zip we get
using (var zipFile = ZipFile.Read(appManager.ZipManager.GetZipStream("site")))
{
zipFile.ExtractAll(tempDirectory);
string settings = File.ReadAllText(Path.Combine(tempDirectory, "wwwroot", "foo.txt"));
Assert.Contains("Hello zip", settings);
}
string barFile = Path.Combine(tempDirectory, "wwwroot", "bar.txt");
File.WriteAllText(barFile, "Kudu zip");
using (var zipFile2 = new ZipFile())
{
zipFile2.AddDirectory(tempDirectory);
zipFile2.Save(tempZipPath);
}
// Upload a zip with an additional file
appManager.ZipManager.PutZipFile("site", tempZipPath);
// Use vfs to make sure it's there
string barContent = appManager.VfsWebRootManager.ReadAllText("bar.txt");
Assert.Contains("Kudu zip", barContent);
}
finally
{
Directory.Delete(tempDirectory, recursive: true);
File.Delete(tempZipPath);
}
});
}
}
}

1 change: 1 addition & 0 deletions Kudu.FunctionalTests/packages.config
@@ -1,5 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<packages>
<package id="DotNetZip" version="1.9.1.8" targetFramework="net45" />
<package id="Microsoft.AspNet.WebApi.Client" version="4.0.20710.0" targetFramework="net45" />
<package id="Microsoft.Net.Http" version="2.0.20710.0" targetFramework="net45" />
<package id="Newtonsoft.Json" version="4.5.11" targetFramework="net45" />
Expand Down
4 changes: 4 additions & 0 deletions Kudu.Services.Web/App_Start/NinjectServices.cs
Expand Up @@ -231,6 +231,10 @@ public static void RegisterRoutes(IKernel kernel, RouteCollection routes)
routes.MapHttpRoute("vfs-put-files", "vfs/{*path}", new { controller = "Vfs", action = "PutItem" }, new { verb = new HttpMethodConstraint("PUT") });
routes.MapHttpRoute("vfs-delete-files", "vfs/{*path}", new { controller = "Vfs", action = "DeleteItem" }, new { verb = new HttpMethodConstraint("DELETE") });

// Zip file handler
routes.MapHttpRoute("zip-get-files", "zip/{*path}", new { controller = "Zip", action = "GetItem" }, new { verb = new HttpMethodConstraint("GET", "HEAD") });
routes.MapHttpRoute("zip-put-files", "zip/{*path}", new { controller = "Zip", action = "PutItem" }, new { verb = new HttpMethodConstraint("PUT") });

// Live Command Line
routes.MapHttpRoute("execute-command", "command", new { controller = "Command", action = "ExecuteCommand" }, new { verb = new HttpMethodConstraint("POST") });

Expand Down
24 changes: 17 additions & 7 deletions Kudu.Services/Infrastructure/VfsControllerBase.cs
Expand Up @@ -64,10 +64,7 @@ public virtual Task<HttpResponseMessage> GetItem()
}
else
{
// Enumerate directory
IEnumerable<VfsStatEntry> directory = GetDirectoryResponse(info, localFilePath);
HttpResponseMessage successDirectoryResponse = Request.CreateResponse<IEnumerable<VfsStatEntry>>(HttpStatusCode.OK, directory);
return Task.FromResult(successDirectoryResponse);
return CreateDirectoryGetResponse(info, localFilePath);
}
}
else
Expand Down Expand Up @@ -96,9 +93,7 @@ public virtual Task<HttpResponseMessage> PutItem()

if (itemExists && (info.Attributes & FileAttributes.Directory) != 0)
{
HttpResponseMessage conflictDirectoryResponse = Request.CreateErrorResponse(
HttpStatusCode.Conflict, Resources.VfsController_CannotUpdateDirectory);
return Task.FromResult(conflictDirectoryResponse);
return CreateDirectoryPutResponse(info, localFilePath);
}
else
{
Expand Down Expand Up @@ -171,8 +166,23 @@ public virtual Task<HttpResponseMessage> DeleteItem()

protected MediaTypeMap MediaTypeMap { get; private set; }

protected virtual Task<HttpResponseMessage> CreateDirectoryGetResponse(DirectoryInfo info, string localFilePath)
{
// Enumerate directory
IEnumerable<VfsStatEntry> directory = GetDirectoryResponse(info, localFilePath);
HttpResponseMessage successDirectoryResponse = Request.CreateResponse<IEnumerable<VfsStatEntry>>(HttpStatusCode.OK, directory);
return Task.FromResult(successDirectoryResponse);
}

protected abstract Task<HttpResponseMessage> CreateItemGetResponse(FileSystemInfo info, string localFilePath);

protected virtual Task<HttpResponseMessage> CreateDirectoryPutResponse(DirectoryInfo info, string localFilePath)
{
HttpResponseMessage conflictDirectoryResponse = Request.CreateErrorResponse(
HttpStatusCode.Conflict, Resources.VfsController_CannotUpdateDirectory);
return Task.FromResult(conflictDirectoryResponse);
}

protected abstract Task<HttpResponseMessage> CreateItemPutResponse(FileSystemInfo info, string localFilePath, bool itemExists);

protected virtual Task<HttpResponseMessage> CreateItemDeleteResponse(FileSystemInfo info, string localFilePath)
Expand Down
1 change: 1 addition & 0 deletions Kudu.Services/Kudu.Services.csproj
Expand Up @@ -177,6 +177,7 @@
</Compile>
<Compile Include="Settings\SettingsController.cs" />
<Compile Include="SourceControl\LiveScmController.cs" />
<Compile Include="Zip\ZipController.cs" />
</ItemGroup>
<ItemGroup>
<None Include="packages.config" />
Expand Down
85 changes: 85 additions & 0 deletions Kudu.Services/Zip/ZipController.cs
@@ -0,0 +1,85 @@
using System.IO;
using System.Net;
using System.Net.Http;
using System.Net.Http.Headers;
using System.Threading.Tasks;
using Ionic.Zip;
using Kudu.Contracts.Tracing;
using Kudu.Core;
using Kudu.Services.Infrastructure;

namespace Kudu.Services.Deployment
{
// Extending VfsControllerBase is a slight abuse since this has nothing to do with vfs. But there is a lot
// of good reusable logic in there. We could consider extracting a more basic base class from it.
public class ZipController : VfsControllerBase
{
public ZipController(ITracer tracer, IEnvironment environment)
: base(tracer, environment, environment.RootPath)
{
}

protected override Task<HttpResponseMessage> CreateDirectoryGetResponse(DirectoryInfo info, string localFilePath)
{
HttpResponseMessage response = Request.CreateResponse();
using (var zip = new ZipFile())
{
foreach (FileSystemInfo fileSysInfo in info.EnumerateFileSystemInfos())
{
bool isDirectory = (fileSysInfo.Attributes & FileAttributes.Directory) != 0;

if (isDirectory)
{
zip.AddDirectory(fileSysInfo.FullName, fileSysInfo.Name);
}
else
{
// Add it at the root of the zip
zip.AddFile(fileSysInfo.FullName, "/");
}
}

using (var ms = new MemoryStream())
{
zip.Save(ms);
response.Content = ms.AsContent();
}
}

response.Content.Headers.ContentType = new MediaTypeHeaderValue("application/zip");
response.Content.Headers.ContentDisposition = new ContentDispositionHeaderValue("attachment");

// Name the zip after the folder. e.g. "c:\foo\bar\" --> "bar"
response.Content.Headers.ContentDisposition.FileName = Path.GetFileName(Path.GetDirectoryName(localFilePath)) + ".zip";
return Task.FromResult(response);
}

protected override Task<HttpResponseMessage> CreateItemGetResponse(FileSystemInfo info, string localFilePath)
{
// We don't support getting a file from the zip controller
// Conceivably, it could be a zip file containing just the one file, but that's rarely interesting
HttpResponseMessage notFoundResponse = Request.CreateResponse(HttpStatusCode.NotFound);
return Task.FromResult(notFoundResponse);
}

protected override async Task<HttpResponseMessage> CreateDirectoryPutResponse(DirectoryInfo info, string localFilePath)
{
using (var zipFile = ZipFile.Read(await Request.Content.ReadAsStreamAsync()))
{
// The unzipping is done over the existing folder, without first removing existing files.
// Hence it's more of a PATCH than a PUT. We should consider supporting both with the right semantic.
// Though a true PUT at the root would be scary as it would wipe all existing files!
zipFile.ExtractAll(localFilePath, ExtractExistingFileAction.OverwriteSilently);
}

return Request.CreateResponse(HttpStatusCode.OK);
}

protected override Task<HttpResponseMessage> CreateItemPutResponse(FileSystemInfo info, string localFilePath, bool itemExists)
{
// We don't support putting an individual file using the zip controller
HttpResponseMessage notFoundResponse = Request.CreateResponse(HttpStatusCode.NotFound);
return Task.FromResult(notFoundResponse);
}
}
}
7 changes: 7 additions & 0 deletions Kudu.TestHarness/ApplicationManager.cs
Expand Up @@ -92,6 +92,12 @@ public RemoteVfsManager LiveScmVfsManager
private set;
}

public RemoteZipManager ZipManager
{
get;
private set;
}

public RemoteCommandExecutor CommandExecutor
{
get;
Expand Down Expand Up @@ -328,6 +334,7 @@ public static ApplicationManager CreateApplication(string applicationName)
VfsManager = new RemoteVfsManager(site.ServiceUrl + "vfs"),
VfsWebRootManager = new RemoteVfsManager(site.ServiceUrl + "vfs/site/wwwroot"),
LiveScmVfsManager = new RemoteVfsManager(site.ServiceUrl + "scmvfs"),
ZipManager = new RemoteZipManager(site.ServiceUrl + "zip"),
CommandExecutor = new RemoteCommandExecutor(site.ServiceUrl + "command"),
RepositoryManager = repositoryManager,
};
Expand Down

0 comments on commit 66d1a51

Please sign in to comment.