Gets local package installation 'working', now needs a little UI work.

This commit is contained in:
Shannon
2016-06-13 18:28:56 +02:00
parent f5d3f9087f
commit c2b016d7d0
9 changed files with 405 additions and 52 deletions

View File

@@ -1,29 +1,127 @@
(function () { (function () {
"use strict"; "use strict";
function PackagesInstallLocalController($scope, $route, $location) { function PackagesInstallLocalController($scope, $route, $location, Upload, umbRequestHelper, packageResource, $cookieStore) {
var vm = this; var vm = this;
vm.state = "upload"; vm.state = "upload";
vm.localPackage = { vm.localPackage = {};
"icon":"https://our.umbraco.org/media/wiki/154472/635997115126742822_logopng.png?bgcolor=fff&height=154&width=281&format=png", vm.loadPackage = loadPackage;
"name": "SvgIconPicker Version: 0.1.0", vm.installPackage = installPackage;
"author": "Søren Kottal", vm.installState = {
"authorLink": "https://github.com/skttl/", status: ""
"info": "https://github.com/skttl/Umbraco.SvgIconPicker", };
"licens": "GPLv3", vm.zipFile = {
"licensLink": "http://www.gnu.org/licenses/quick-guide-gplv3.en.html", uploadStatus: "idle",
"licensAccept": false, uploadProgress: 0,
"readme": "Color Palettes is a simple property editor that let you define different color palettes (or get them from Adobe Kuler or COLOURlovers) and present them to the editor as a list of radio buttons.", serverErrorMessage: null
"filePath": "",
"riskAccept": false
}; };
vm.loadPackage = loadPackage; $scope.handleFiles = function (files, event) {
for (var i = 0; i < files.length; i++) {
upload(files[i]);
}
};
function loadPackage(){ function upload(file) {
vm.state = "packageDetails";
Upload.upload({
url: umbRequestHelper.getApiUrl("packageInstallApiBaseUrl", "UploadLocalPackage"),
fields: {},
file: file
}).progress(function (evt) {
// calculate progress in percentage
var progressPercentage = parseInt(100.0 * evt.loaded / evt.total, 10);
// set percentage property on file
vm.zipFile.uploadProgress = progressPercentage;
// set uploading status on file
vm.zipFile.uploadStatus = "uploading";
}).success(function (data, status, headers, config) {
if (data.notifications && data.notifications.length > 0) {
// set error status on file
vm.zipFile.uploadStatus = "error";
// Throw message back to user with the cause of the error
vm.zipFile.serverErrorMessage = data.notifications[0].message;
//TODO: Handle the error in UI
} else {
// set done status on file
vm.zipFile.uploadStatus = "done";
vm.localPackage = data;
}
}).error(function (evt, status, headers, config) {
//TODO: Handle the error in UI
// set status done
vm.zipFile.uploadStatus = "error";
//if the service returns a detailed error
if (evt.InnerException) {
vm.zipFile.serverErrorMessage = evt.InnerException.ExceptionMessage;
//Check if its the common "too large file" exception
if (evt.InnerException.StackTrace && evt.InnerException.StackTrace.indexOf("ValidateRequestEntityLength") > 0) {
vm.zipFile.serverErrorMessage = "File too large to upload";
}
} else if (evt.Message) {
file.serverErrorMessage = evt.Message;
}
// If file not found, server will return a 404 and display this message
if (status === 404) {
vm.zipFile.serverErrorMessage = "File not found";
}
});
}
function loadPackage() {
if (vm.zipFile.uploadStatus === "done") {
vm.state = "packageDetails";
}
}
function installPackage() {
vm.installState.status = "Installing";
//TODO: If any of these fail, will they keep calling the next one?
packageResource
.installFiles(vm.localPackage)
.then(function(pack) {
vm.installState.status = "Restarting, please hold...";
return packageResource.installData(pack);
},
installError)
.then(function(pack) {
vm.installState.status = "All done, your browser will now refresh";
return packageResource.cleanUp(pack);
},
installError)
.then(installComplete, installError);
}
function installComplete() {
var url = window.location.href + "?installed=" + vm.localPackage.packageGuid;
$cookieStore.put("umbPackageInstallId", vm.localPackage.packageGuid);
window.location.reload(true);
}
function installError() {
} }
} }

View File

@@ -6,10 +6,43 @@
<div class="umb-upload-field"> <div class="umb-upload-field">
<i class="icon-box"></i>
<small class="faded"><strong>Drop to upload</strong></small>
</div> <!-- Drag and drop files area -->
<div ngf-drop
ng-hide="hideDropzone"
ng-model="filesHolder"
ngf-change="handleFiles($files, $event)"
class="dropzone"
ngf-drag-over-class="drag-over"
ngf-multiple="false"
ngf-allow-dir="false"
ngf-pattern="*.zip"
ngf-max-size="{{ maxFileSize }}"
ng-class="{'is-small': compact!=='false' || (done.length+queue.length) > 0 }">
<div class="content">
<!-- Drag and drop illustration -->
<i class="icon-box" draggable="false"></i>
<small class="faded" draggable="false"><strong>Drop to upload</strong></small>
<!-- Select files -->
<div class="file-select"
ngf-select
ng-model="filesHolder"
ngf-change="handleFiles($files, $event)"
ngf-multiple="true"
ngf-pattern="*.zip"
ngf-max-size="{{ maxFileSize }}">
- <localize key="media_orClickHereToUpload">or click here to choose files</localize>
</div>
</div>
</div>
</div>
<h3><strong>Upload package</strong></h3> <h3><strong>Upload package</strong></h3>
<p class="faded"> <p class="faded">
Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam
@@ -18,6 +51,8 @@
<button type="button" <button type="button"
class="umb-era-button -blue" class="umb-era-button -blue"
ng-disabled="vm.zipFile.uploadStatus !== 'done'"
ng-class="{'-inactive' : vm.zipFile.uploadStatus !== 'done'}"
ng-click="vm.loadPackage()">Next ng-click="vm.loadPackage()">Next
</button> </button>
</form> </form>
@@ -49,7 +84,7 @@
<div class="umb-info-local-item"> <div class="umb-info-local-item">
<strong>License</strong> <strong>License</strong>
<a href="{{ vm.localPackage.licensLink }}">{{ vm.localPackage.licens }}</a> <a href="{{ vm.localPackage.licenseLink }}">{{ vm.localPackage.license }}</a>
</div> </div>
<div class="umb-info-local-item"> <div class="umb-info-local-item">
@@ -58,18 +93,21 @@
<small> {{ vm.localPackage.readme }} </small> <small> {{ vm.localPackage.readme }} </small>
</div> </div>
<div class="umb-info-local-item mt-3"> <div class="umb-info-local-item mt-3" ng-if="vm.installState.status == ''">
<button type="button" <button type="button"
ng-class="{'-inactive' : localPackageForm.$invalid}" ng-class="{'-inactive' : localPackageForm.$invalid}"
ng-disabled="localPackageForm.$invalid" ng-disabled="localPackageForm.$invalid"
class="umb-era-button -blue inline-flex"> class="umb-era-button -blue inline-flex"
ng-click="vm.installPackage()">
Install package Install package
</button> </button>
<label for="licens-accept" class="umb-package-installer-label"> <label for="license-accept" class="umb-package-installer-label">
<input type="checkbox" id="licens-accept" ng-model="vm.localPackage.packageLicensAccept" required> <input type="checkbox" id="license-accept" ng-model="vm.localPackage.packageLicenseAccept" required>
<strong class="label-text">I accept <a href="#">terms of use</a></strong> <strong class="label-text">I accept <a href="#">terms of use</a></strong>
</label> </label>
</div>
<div class="umb-info-local-item">
<p>{{vm.installState.status}}</p>
</div> </div>
</div> </div>

View File

@@ -12,7 +12,7 @@
</div> </div>
<div ng-show="vm.loading === false"> <div ng-show="vm.loading === false">
<div class="umb-packages-section"> <div class="umb-packages-section" ng-if="vm.searchQuery == ''">
<div class="umb-packages-categories"> <div class="umb-packages-categories">
<a href="" <a href=""
class="umb-packages-category" class="umb-packages-category"
@@ -37,7 +37,7 @@
<div class="umb-package-info"> <div class="umb-package-info">
<div class="umb-package-name">{{ package.name }}</div> <div class="umb-package-name">{{ package.name }}</div>
<div class="umb-package-description">{{ package.description | limitTo: 40 }}<span ng-if="package.description > (package.description | limitTo: 40)">...</span></div> <div class="umb-package-description">{{ package.excerpt | limitTo: 40 }}<span ng-if="package.excerpt > (package.excerpt | limitTo: 40)">...</span></div>
<div class="umb-package-numbers"> <div class="umb-package-numbers">
<small class="umb-package-downloads"> <small class="umb-package-downloads">
@@ -68,7 +68,7 @@
<div class="umb-package-info"> <div class="umb-package-info">
<div class="umb-package-name">{{ package.name }}</div> <div class="umb-package-name">{{ package.name }}</div>
<div class="umb-package-description">{{ package.description | limitTo: 40 }}<span ng-if="package.description > (package.description | limitTo: 40)">...</span></div> <div class="umb-package-description">{{ package.excerpt | limitTo: 40 }}<span ng-if="package.excerpt > (package.excerpt | limitTo: 40)">...</span></div>
<div class="umb-package-numbers"> <div class="umb-package-numbers">
<small class="umb-package-downloads"> <small class="umb-package-downloads">
@@ -86,7 +86,7 @@
</div> <!-- end packages --> </div> <!-- end packages -->
</div> </div>
<div class="umb-packages__pagination" ng-if="vm.pagination.totalPages > 1"> <div class="umb-packages__pagination" ng-if="vm.pagination.totalPages > 1 && vm.loading === false">
<umb-pagination page-number="vm.pagination.pageNumber" <umb-pagination page-number="vm.pagination.pageNumber"
total-pages="vm.pagination.totalPages" total-pages="vm.pagination.totalPages"

View File

@@ -594,25 +594,7 @@ namespace Umbraco.Web.Editors
return Request.CreateResponse(HttpStatusCode.OK, tempFiles); return Request.CreateResponse(HttpStatusCode.OK, tempFiles);
} }
/// <summary>
/// This is used for the response of PostAddFile so that we can analyze the response in a filter and remove the
/// temporary files that were created.
/// </summary>
[DataContract]
private class PostedFiles : IHaveUploadedFiles, INotificationModel
{
public PostedFiles()
{
UploadedFiles = new List<ContentItemFile>();
Notifications = new List<Notification>();
}
public List<ContentItemFile> UploadedFiles { get; private set; }
[DataMember(Name = "notifications")]
public List<Notification> Notifications { get; private set; }
}
/// <summary> /// <summary>
/// Ensures the item can be moved/copied to the new location /// Ensures the item can be moved/copied to the new location
/// </summary> /// </summary>

View File

@@ -4,6 +4,7 @@ using System.Globalization;
using System.IO; using System.IO;
using System.IO.Compression; using System.IO.Compression;
using System.Linq; using System.Linq;
using System.Net;
using System.Net.Http; using System.Net.Http;
using System.Text; using System.Text;
using System.Threading.Tasks; using System.Threading.Tasks;
@@ -13,11 +14,16 @@ using System.Xml.Linq;
using Umbraco.Core.Auditing; using Umbraco.Core.Auditing;
using umbraco.BusinessLogic; using umbraco.BusinessLogic;
using umbraco.cms.businesslogic.packager.repositories; using umbraco.cms.businesslogic.packager.repositories;
using Umbraco.Core;
using Umbraco.Core.Configuration;
using Umbraco.Core.IO; using Umbraco.Core.IO;
using Umbraco.Core.Packaging.Models; using Umbraco.Core.Packaging.Models;
using Umbraco.Core.Services; using Umbraco.Core.Services;
using Umbraco.Web.Models; using Umbraco.Web.Models;
using Umbraco.Web.Models.ContentEditing;
using Umbraco.Web.Mvc; using Umbraco.Web.Mvc;
using Umbraco.Web.UI;
using Umbraco.Web.WebApi;
using Umbraco.Web.WebApi.Filters; using Umbraco.Web.WebApi.Filters;
namespace Umbraco.Web.Editors namespace Umbraco.Web.Editors
@@ -26,7 +32,117 @@ namespace Umbraco.Web.Editors
[UmbracoApplicationAuthorize(Core.Constants.Applications.Developer)] [UmbracoApplicationAuthorize(Core.Constants.Applications.Developer)]
public class PackageInstallController : UmbracoAuthorizedJsonController public class PackageInstallController : UmbracoAuthorizedJsonController
{ {
[HttpPost]
[FileUploadCleanupFilter(false)]
public async Task<LocalPackageInstallModel> UploadLocalPackage()
{
if (Request.Content.IsMimeMultipartContent() == false)
{
throw new HttpResponseException(HttpStatusCode.UnsupportedMediaType);
}
var root = IOHelper.MapPath("~/App_Data/TEMP/FileUploads");
//ensure it exists
Directory.CreateDirectory(root);
var provider = new MultipartFormDataStreamProvider(root);
var result = await Request.Content.ReadAsMultipartAsync(provider);
//must have a file
if (result.FileData.Count == 0)
{
throw new HttpResponseException(Request.CreateResponse(HttpStatusCode.NotFound));
}
//TODO: App/Tree Permissions?
var model = new LocalPackageInstallModel
{
PackageGuid = Guid.NewGuid()
};
//get the files
foreach (var file in result.FileData)
{
var fileName = file.Headers.ContentDisposition.FileName.Trim(new[] { '\"' });
var ext = fileName.Substring(fileName.LastIndexOf('.') + 1).ToLower();
//TODO: Only allow .zip
if (ext.InvariantEquals("zip") || ext.InvariantEquals("umb"))
{
//TODO: Currently it has to be here, it's not ideal but that's the way it is right now
var packageTempDir = IOHelper.MapPath(SystemDirectories.Data);
//ensure it's there
Directory.CreateDirectory(packageTempDir);
//copy it - must always be '.umb' for the installer thing to work
//the file name must be a GUID - this is what the packager expects (strange yes)
//because essentially this is creating a temporary package Id that will be used
//for unpacking/installing/etc...
var packageTempFileName = model.PackageGuid + ".umb";
var packageTempFileLocation = Path.Combine(packageTempDir, packageTempFileName);
File.Copy(file.LocalFileName, packageTempFileLocation, true);
try
{
var ins = new global::umbraco.cms.businesslogic.packager.Installer(Security.CurrentUser.Id);
//this will load in all the metadata too
var tempDir = ins.Import(packageTempFileName);
model.TemporaryDirectoryPath = Path.Combine(SystemDirectories.Data, tempDir);
model.Id = ins.CreateManifest(
IOHelper.MapPath(model.TemporaryDirectoryPath),
model.PackageGuid.ToString(),
//TODO: Does this matter? we're installing a local package
string.Empty);
model.Name = ins.Name;
model.Author = ins.Author;
model.AuthorUrl = ins.AuthorUrl;
model.License = ins.License;
model.LicenseUrl = ins.LicenseUrl;
model.ReadMe = ins.ReadMe;
model.ConflictingMacroAliases = ins.ConflictingMacroAliases;
model.ConflictingStyleSheetNames = ins.ConflictingStyleSheetNames;
model.ConflictingTemplateAliases = ins.ConflictingTemplateAliases;
model.ContainsBinaryFileErrors = ins.ContainsBinaryFileErrors;
model.ContainsLegacyPropertyEditors = ins.ContainsLegacyPropertyEditors;
model.ContainsMacroConflict = ins.ContainsMacroConflict;
model.ContainsStyleSheetConflicts = ins.ContainsStyleSheeConflicts;
model.ContainsTemplateConflicts = ins.ContainsTemplateConflicts;
model.ContainsUnsecureFiles = ins.ContainsUnsecureFiles;
model.Url = ins.Url;
model.Version = ins.Version;
//TODO: We need to add the 'strict' requirement to the installer
}
finally
{
//Cleanup file
if (File.Exists(packageTempFileLocation))
{
File.Delete(packageTempFileLocation);
}
}
}
else
{
model.Notifications.Add(new Notification(
Services.TextService.Localize("speechBubbles/operationFailedHeader"),
Services.TextService.Localize("media/disallowedFileType"),
SpeechBubbleIcon.Warning));
}
}
return model;
}
/// <summary>
/// Gets the package from Our to install
/// </summary>
/// <param name="packageGuid"></param>
/// <returns></returns>
[HttpGet] [HttpGet]
public PackageInstallModel Fetch(string packageGuid) public PackageInstallModel Fetch(string packageGuid)
{ {
@@ -50,6 +166,11 @@ namespace Umbraco.Web.Editors
return p; return p;
} }
/// <summary>
/// Extracts the package zip and gets the packages information
/// </summary>
/// <param name="model"></param>
/// <returns></returns>
[HttpPost] [HttpPost]
public PackageInstallModel Import(PackageInstallModel model) public PackageInstallModel Import(PackageInstallModel model)
{ {
@@ -60,6 +181,11 @@ namespace Umbraco.Web.Editors
return model; return model;
} }
/// <summary>
/// Installs the package files
/// </summary>
/// <param name="model"></param>
/// <returns></returns>
[HttpPost] [HttpPost]
public PackageInstallModel InstallFiles(PackageInstallModel model) public PackageInstallModel InstallFiles(PackageInstallModel model)
{ {
@@ -69,7 +195,11 @@ namespace Umbraco.Web.Editors
return model; return model;
} }
/// <summary>
/// Installs the packages data/business logic
/// </summary>
/// <param name="model"></param>
/// <returns></returns>
[HttpPost] [HttpPost]
public PackageInstallModel InstallData(PackageInstallModel model) public PackageInstallModel InstallData(PackageInstallModel model)
{ {
@@ -79,7 +209,11 @@ namespace Umbraco.Web.Editors
return model; return model;
} }
/// <summary>
/// Cleans up the package installation
/// </summary>
/// <param name="model"></param>
/// <returns></returns>
[HttpPost] [HttpPost]
public PackageInstallModel CleanUp(PackageInstallModel model) public PackageInstallModel CleanUp(PackageInstallModel model)
{ {

View File

@@ -0,0 +1,23 @@
using System.Collections.Generic;
using System.Runtime.Serialization;
namespace Umbraco.Web.Models.ContentEditing
{
/// <summary>
/// This is used for the response of PostAddFile so that we can analyze the response in a filter and remove the
/// temporary files that were created.
/// </summary>
[DataContract]
internal class PostedFiles : IHaveUploadedFiles, INotificationModel
{
public PostedFiles()
{
UploadedFiles = new List<ContentItemFile>();
Notifications = new List<Notification>();
}
public List<ContentItemFile> UploadedFiles { get; private set; }
[DataMember(Name = "notifications")]
public List<Notification> Notifications { get; private set; }
}
}

View File

@@ -0,0 +1,75 @@
using System.Collections.Generic;
using System.Runtime.Serialization;
using Umbraco.Web.Models.ContentEditing;
namespace Umbraco.Web.Models
{
/// <summary>
/// A model that represents uploading a local package
/// </summary>
[DataContract(Name = "localPackageInstallModel")]
public class LocalPackageInstallModel : PackageInstallModel, IHaveUploadedFiles, INotificationModel
{
public LocalPackageInstallModel()
{
UploadedFiles = new List<ContentItemFile>();
Notifications = new List<Notification>();
}
public List<ContentItemFile> UploadedFiles { get; private set; }
[DataMember(Name = "notifications")]
public List<Notification> Notifications { get; private set; }
[DataMember(Name = "name")]
public string Name { get; set; }
[DataMember(Name = "url")]
public string Url { get; set; }
[DataMember(Name = "version")]
public string Version { get; set; }
[DataMember(Name = "containsUnsecureFiles")]
public bool ContainsUnsecureFiles { get; set; }
[DataMember(Name = "containsTemplateConflicts")]
public bool ContainsTemplateConflicts { get; set; }
[DataMember(Name = "containsStyleSheetConflicts")]
public bool ContainsStyleSheetConflicts { get; set; }
[DataMember(Name = "containsMacroConflict")]
public bool ContainsMacroConflict { get; set; }
[DataMember(Name = "containsLegacyPropertyEditors")]
public bool ContainsLegacyPropertyEditors { get; set; }
[DataMember(Name = "containsBinaryFileErrors")]
public bool ContainsBinaryFileErrors { get; set; }
[DataMember(Name = "conflictingTemplateAliases")]
public IDictionary<string, string> ConflictingTemplateAliases { get; set; }
[DataMember(Name = "conflictingStyleSheetNames")]
public IDictionary<string, string> ConflictingStyleSheetNames { get; set; }
[DataMember(Name = "conflictingMacroAliases")]
public IDictionary<string, string> ConflictingMacroAliases { get; set; }
[DataMember(Name = "readMe")]
public string ReadMe { get; set; }
[DataMember(Name = "licenseUrl")]
public string LicenseUrl { get; set; }
[DataMember(Name = "license")]
public string License { get; set; }
[DataMember(Name = "authorUrl")]
public string AuthorUrl { get; set; }
[DataMember(Name = "author")]
public string Author { get; set; }
}
}

View File

@@ -1,5 +1,4 @@
using System; using System;
using System.Collections.Generic;
using System.Linq; using System.Linq;
using System.Runtime.Serialization; using System.Runtime.Serialization;
using System.Text; using System.Text;
@@ -10,7 +9,7 @@ namespace Umbraco.Web.Models
[DataContract(Name = "packageInstallModel")] [DataContract(Name = "packageInstallModel")]
public class PackageInstallModel public class PackageInstallModel
{ {
[DataMember(Name="id")] [DataMember(Name = "id")]
public int Id { get; set; } public int Id { get; set; }
[DataMember(Name = "packageGuid")] [DataMember(Name = "packageGuid")]
@@ -24,5 +23,7 @@ namespace Umbraco.Web.Models
[DataMember(Name = "zipFilePath")] [DataMember(Name = "zipFilePath")]
public string ZipFilePath { get; set; } public string ZipFilePath { get; set; }
} }
} }

View File

@@ -336,9 +336,11 @@
<Compile Include="Models\ContentEditing\MemberPropertyTypeDisplay.cs" /> <Compile Include="Models\ContentEditing\MemberPropertyTypeDisplay.cs" />
<Compile Include="Models\ContentEditing\MemberTypeDisplay.cs" /> <Compile Include="Models\ContentEditing\MemberTypeDisplay.cs" />
<Compile Include="Models\ContentEditing\MemberTypeSave.cs" /> <Compile Include="Models\ContentEditing\MemberTypeSave.cs" />
<Compile Include="Models\ContentEditing\PostedFiles.cs" />
<Compile Include="Models\ContentEditing\PropertyGroupBasic.cs" /> <Compile Include="Models\ContentEditing\PropertyGroupBasic.cs" />
<Compile Include="Models\ContentEditing\PropertyTypeBasic.cs" /> <Compile Include="Models\ContentEditing\PropertyTypeBasic.cs" />
<Compile Include="Models\ContentEditing\SimpleNotificationModel.cs" /> <Compile Include="Models\ContentEditing\SimpleNotificationModel.cs" />
<Compile Include="Models\LocalPackageInstallModel.cs" />
<Compile Include="Models\Mapping\ContentTypeModelMapperExtensions.cs" /> <Compile Include="Models\Mapping\ContentTypeModelMapperExtensions.cs" />
<Compile Include="Models\Mapping\LockedCompositionsResolver.cs" /> <Compile Include="Models\Mapping\LockedCompositionsResolver.cs" />
<Compile Include="Models\Mapping\PropertyGroupDisplayResolver.cs" /> <Compile Include="Models\Mapping\PropertyGroupDisplayResolver.cs" />