Add swift object copy function
Current Horizon does not have object copy page for swift. This blueprint is for adding above one as Angular application. - One row action is added as "Copy" menu - User can copy which bytes > 0 Because bytes = 0 means, true 0 byte object or separated file. Copying of separated object is not allowed in Swift API. Change-Id: I60d731f79e5c9ab55fd2a73b7687b47723fe901f Implements: blueprint swift-object-copy-function
This commit is contained in:
parent
d09170e02a
commit
60a78e1958
@ -276,11 +276,14 @@ def swift_copy_object(request, orig_container_name, orig_object_name,
|
||||
|
||||
headers = {"X-Copy-From": FOLDER_DELIMITER.join([orig_container_name,
|
||||
orig_object_name])}
|
||||
return swift_api(request).put_object(new_container_name,
|
||||
etag = swift_api(request).put_object(new_container_name,
|
||||
new_object_name,
|
||||
None,
|
||||
headers=headers)
|
||||
|
||||
obj_info = {'name': new_object_name, 'etag': etag}
|
||||
return StorageObject(obj_info, new_container_name)
|
||||
|
||||
|
||||
@profiler.trace
|
||||
def swift_upload_object(request, container_name, object_name,
|
||||
|
@ -0,0 +1,132 @@
|
||||
/**
|
||||
* Licensed under the Apache License, Version 2.0 (the 'License');
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an 'AS IS' BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
(function () {
|
||||
'use strict';
|
||||
|
||||
/**
|
||||
* @ngdoc directive
|
||||
* @name check-copy-destination
|
||||
* @element
|
||||
* @description
|
||||
* The `check-copy-destination` directive is used on an angular form
|
||||
* element to verify whether a copy destination is valid or not.
|
||||
*
|
||||
* This directive is called if value of dest_container or dest_name is changed,
|
||||
* then check following.
|
||||
* - destination container correctly exists or not.
|
||||
* - destination object does not exists.(To prevent over writeby mistake)
|
||||
*/
|
||||
angular
|
||||
.module('horizon.dashboard.project.containers')
|
||||
.directive('checkCopyDestination', CheckCopyDestination);
|
||||
|
||||
CheckCopyDestination.$inject = [
|
||||
'horizon.app.core.openstack-service-api.swift',
|
||||
'horizon.dashboard.project.containers.containers-model',
|
||||
'$q'
|
||||
];
|
||||
|
||||
function CheckCopyDestination(swiftAPI, model, $q) {
|
||||
|
||||
/**
|
||||
* functions that is used from inside of directive.
|
||||
* These function will return just exist or not as true or false.
|
||||
*/
|
||||
function checkContainer(container) {
|
||||
var def = $q.defer();
|
||||
swiftAPI
|
||||
.getContainer(container, true)
|
||||
.then(def.resolve, def.reject);
|
||||
return def.promise;
|
||||
}
|
||||
|
||||
function checkObject(container, object) {
|
||||
var def = $q.defer();
|
||||
swiftAPI
|
||||
.getObjectDetails(container, object, true)
|
||||
.then(def.resolve, def.reject);
|
||||
return def.promise;
|
||||
}
|
||||
|
||||
return {
|
||||
restrict: 'A',
|
||||
require: 'ngModel',
|
||||
link: function(scope, element, attrs, ngModel) {
|
||||
|
||||
var ctrl = scope.ctrl;
|
||||
|
||||
scope.$watch(function() {
|
||||
/**
|
||||
* function that returns watching target.
|
||||
* In this case, if either dest_container or dest_name is changed,
|
||||
* second argument(this is also function) will be called.
|
||||
* 3rd argment(true) means watch element of return value from 1st argument.
|
||||
* (=not only reference to array)
|
||||
*/
|
||||
var destContainer = (ctrl.model.dest_container === undefined ||
|
||||
ctrl.model.dest_container === null) ? "" : ctrl.model.dest_container;
|
||||
var destName = (ctrl.model.dest_name === undefined ||
|
||||
ctrl.model.dest_name === null) ? "" : ctrl.model.dest_name;
|
||||
return [destContainer, destName];
|
||||
|
||||
}, function (value) {
|
||||
/**
|
||||
* These function set validity according to
|
||||
* API execution result.
|
||||
*
|
||||
* If exepected value is "exist" like contianer,
|
||||
* error will not be set if object (correctly) exist.
|
||||
*
|
||||
* If exepected value is "does not exist" like object,
|
||||
* error will be set if object exist.
|
||||
*/
|
||||
var destContainer = value[0];
|
||||
var destName = value[1];
|
||||
|
||||
ngModel.$setValidity('containerNotFound', true);
|
||||
ngModel.$setValidity('objectExists', true);
|
||||
|
||||
if (destContainer === "") {
|
||||
return value;
|
||||
}
|
||||
|
||||
checkContainer(destContainer).then(
|
||||
function success() {
|
||||
ngModel.$setValidity('containerNotFound', true);
|
||||
return value;
|
||||
},
|
||||
function error() {
|
||||
ngModel.$setValidity('containerNotFound', false);
|
||||
return;
|
||||
}
|
||||
);
|
||||
|
||||
if (destName !== "") {
|
||||
checkObject(destContainer, destName).then(
|
||||
function success() {
|
||||
ngModel.$setValidity('objectExists', false);
|
||||
return;
|
||||
},
|
||||
function error () {
|
||||
ngModel.$setValidity('objectExists', true);
|
||||
return value;
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
}, true);
|
||||
}
|
||||
};
|
||||
}
|
||||
})();
|
@ -0,0 +1,135 @@
|
||||
/**
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
(function() {
|
||||
'use strict';
|
||||
|
||||
describe('horizon.dashboard.project.containers check-copy-destination.directive', function() {
|
||||
beforeEach(module('horizon.app.core.openstack-service-api'));
|
||||
beforeEach(module('horizon.dashboard.project.containers'));
|
||||
beforeEach(module('horizon.framework'));
|
||||
|
||||
var $compile, $scope, model, element, swiftAPI, apiDeferred;
|
||||
|
||||
beforeEach(inject(function inject($injector, _$q_, _$rootScope_) {
|
||||
$scope = _$rootScope_.$new();
|
||||
$compile = $injector.get('$compile');
|
||||
model = $injector.get('horizon.dashboard.project.containers.containers-model');
|
||||
swiftAPI = $injector.get('horizon.app.core.openstack-service-api.swift');
|
||||
apiDeferred = _$q_.defer();
|
||||
spyOn(swiftAPI, 'getContainer').and.returnValue(apiDeferred.promise);
|
||||
spyOn(swiftAPI, 'getObjectDetails').and.returnValue(apiDeferred.promise);
|
||||
model.container = {name: 'spam'};
|
||||
model.folder = 'ham';
|
||||
|
||||
$scope.ctrl = {
|
||||
model: {
|
||||
dest_container: '',
|
||||
dest_name: ''
|
||||
}
|
||||
};
|
||||
|
||||
element = angular.element(
|
||||
'<div ng-form="copyForm">' +
|
||||
'<input id="id_dest_container" name="dest_container" type="text" ' +
|
||||
'check-copy-destination ng-model="ctrl.model.dest_container" />' +
|
||||
'<span id="id_span_dest_container" ' +
|
||||
'ng-show="copyForm.dest_container.$error.containerNotFound">' +
|
||||
'Container does not exist</span>' +
|
||||
'' +
|
||||
'<input id="id_dest_name" name="dest_name" type="text"' +
|
||||
' check-copy-destination ng-model="ctrl.model.dest_name" />' +
|
||||
'<span id="id_span_dest_name" ' +
|
||||
'ng-show="copyForm.dest_name.$error.objectExists">' +
|
||||
'Object already exists</span>' +
|
||||
'</div>'
|
||||
);
|
||||
element = $compile(element)($scope);
|
||||
$scope.$apply();
|
||||
}));
|
||||
|
||||
it('should accept container name that exists', function test() {
|
||||
// input field value
|
||||
var containerName = 'someContainerName';
|
||||
element.find('input#id_dest_container').val(containerName).trigger('input');
|
||||
expect(swiftAPI.getContainer).toHaveBeenCalledWith(containerName, true);
|
||||
|
||||
// In case resolve() returned, it means specified container
|
||||
// correctly exists. so error <span> for container should not be displayed.
|
||||
apiDeferred.resolve();
|
||||
$scope.$apply();
|
||||
expect(element.find('#id_span_dest_container').hasClass('ng-hide')).toEqual(true);
|
||||
});
|
||||
|
||||
it('should accept container name that dees not exist', function test() {
|
||||
// input field value
|
||||
var containerName = 'someContainerName';
|
||||
element.find('input#id_dest_container').val(containerName).trigger('input');
|
||||
expect(swiftAPI.getContainer).toHaveBeenCalledWith(containerName, true);
|
||||
|
||||
// In case reject() returned, it means specified container
|
||||
// does not exist. so error <span> for container should be displayed.
|
||||
apiDeferred.reject();
|
||||
$scope.$apply();
|
||||
expect(element.find('#id_span_dest_container').hasClass('ng-hide')).toEqual(false);
|
||||
});
|
||||
|
||||
it('should not accept object already exists to prevent overwrite of object', function test() {
|
||||
// input field value (destination container)
|
||||
var containerName = 'someContainerName';
|
||||
element.find('input#id_dest_container').val(containerName).trigger('input');
|
||||
expect(swiftAPI.getContainer).toHaveBeenCalledWith(containerName, true);
|
||||
|
||||
// In case resolve() returned, it means specified container
|
||||
// correctly exists. so error <span> for container should not be displayed.
|
||||
apiDeferred.resolve();
|
||||
$scope.$apply();
|
||||
|
||||
// input field value (destination object)
|
||||
var objectName = 'someObjectName';
|
||||
element.find('input#id_dest_name').val(objectName).trigger('input');
|
||||
expect(swiftAPI.getObjectDetails).toHaveBeenCalledWith(containerName, objectName, true);
|
||||
|
||||
apiDeferred.resolve();
|
||||
$scope.$apply();
|
||||
|
||||
// In case resolve() returned, it means specified object
|
||||
// already exists. so error <span> for object should be displayed.
|
||||
expect(element.find('#id_span_dest_name').hasClass('ng-hide')).toEqual(false);
|
||||
});
|
||||
|
||||
it('should accept object name does not exist', function test() {
|
||||
// input field value (destination container)
|
||||
var containerName = 'someContainerName';
|
||||
element.find('input#id_dest_container').val(containerName).trigger('input');
|
||||
expect(swiftAPI.getContainer).toHaveBeenCalledWith(containerName, true);
|
||||
|
||||
// In case resolve() returned, it means specified container
|
||||
// correctly exists. so error <span> for container should not be displayed.
|
||||
apiDeferred.resolve();
|
||||
$scope.$apply();
|
||||
|
||||
// input field value (destination object)
|
||||
var objectName = 'someObjectName';
|
||||
element.find('input#id_dest_name').val(objectName).trigger('input');
|
||||
expect(swiftAPI.getObjectDetails).toHaveBeenCalledWith(containerName, objectName, true);
|
||||
|
||||
apiDeferred.reject();
|
||||
$scope.$apply();
|
||||
|
||||
// In case resolve() returned, it means specified object
|
||||
// already exists. so error <span> for object should be displayed.
|
||||
expect(element.find('#id_span_dest_name').hasClass('ng-hide')).toEqual(false);
|
||||
});
|
||||
});
|
||||
})();
|
@ -0,0 +1,36 @@
|
||||
/**
|
||||
* Licensed under the Apache License, Version 2.0 (the 'License');
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an 'AS IS' BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
(function () {
|
||||
'use strict';
|
||||
angular
|
||||
.module('horizon.dashboard.project.containers')
|
||||
.controller(
|
||||
'horizon.dashboard.project.containers.CopyObjectModalController',
|
||||
CopyObjectModalController
|
||||
);
|
||||
|
||||
CopyObjectModalController.$inject = [
|
||||
'fileDetails'
|
||||
];
|
||||
|
||||
function CopyObjectModalController(fileDetails) {
|
||||
var ctrl = this;
|
||||
ctrl.model = {
|
||||
container: fileDetails.container,
|
||||
path: fileDetails.path,
|
||||
dest_container: "",
|
||||
dest_name: ""
|
||||
};
|
||||
}
|
||||
})();
|
@ -0,0 +1,43 @@
|
||||
/**
|
||||
* Licensed under the Apache License, Version 2.0 (the "License"); you may
|
||||
* not use this file except in compliance with the License. You may obtain
|
||||
* a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
* License for the specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
|
||||
(function() {
|
||||
'use strict';
|
||||
|
||||
describe('horizon.dashboard.project.containers copy-object controller', function() {
|
||||
var controller, ctrl;
|
||||
|
||||
beforeEach(module('horizon.framework'));
|
||||
beforeEach(module('horizon.dashboard.project.containers'));
|
||||
|
||||
beforeEach(module(function ($provide) {
|
||||
$provide.value('fileDetails', {
|
||||
container: 'spam',
|
||||
path: 'ham/eggs'
|
||||
});
|
||||
}));
|
||||
|
||||
beforeEach(inject(function ($injector) {
|
||||
controller = $injector.get('$controller');
|
||||
ctrl = controller('horizon.dashboard.project.containers.CopyObjectModalController');
|
||||
}));
|
||||
|
||||
it('should initialise the controller model when created', function test() {
|
||||
expect(ctrl.model.container).toEqual('spam');
|
||||
expect(ctrl.model.path).toEqual('ham/eggs');
|
||||
expect(ctrl.model.dest_container).toEqual('');
|
||||
expect(ctrl.model.dest_name).toEqual('');
|
||||
});
|
||||
});
|
||||
})();
|
@ -0,0 +1,106 @@
|
||||
<div ng-form="copyForm">
|
||||
|
||||
<div class="modal-header">
|
||||
<button type="button" class="close" ng-click="$dismiss()" aria-hidden="true" aria-label="Close">
|
||||
<span aria-hidden="true" class="fa fa-times"></span>
|
||||
</button>
|
||||
<div class="h3 modal-title">
|
||||
<translate>Copy Object: {$ ctrl.model.container $}/{$ ctrl.model.path $}</translate>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="modal-body">
|
||||
<div class="row">
|
||||
<div class="col-sm-6">
|
||||
<fieldset>
|
||||
|
||||
<!-- Destination Container -->
|
||||
<div class="form-group required"
|
||||
ng-class="{'has-error': copyForm.dest_container.$invalid && copyForm.dest_container.$dirty}">
|
||||
<label class="control-label required" for="id_dest_container" translate>
|
||||
Destination Container
|
||||
</label>
|
||||
<span class="hz-icon-required fa fa-asterisk"></span>
|
||||
<div>
|
||||
<input class="form-control"
|
||||
id="id_dest_container"
|
||||
name="dest_container"
|
||||
type="text"
|
||||
maxlength="255"
|
||||
ng-model="ctrl.model.dest_container"
|
||||
ng-required="true"
|
||||
ng-model-options="{ debounce: 1000 }"
|
||||
check-copy-destination
|
||||
>
|
||||
</div>
|
||||
<span class="help-block"
|
||||
ng-show="copyForm.dest_container.$error.required && copyForm.dest_container.$dirty"
|
||||
translate>
|
||||
This field is required.
|
||||
</span>
|
||||
<span class="help-block text-danger"
|
||||
ng-show="copyForm.dest_container.$error.containerNotFound"
|
||||
translate>
|
||||
This container does not exist.
|
||||
</span>
|
||||
</div>
|
||||
|
||||
|
||||
<!-- Destination Object -->
|
||||
<div class="form-group required"
|
||||
ng-class="{'has-error': copyForm.dest_name.$invalid && copyForm.dest_name.$dirty}">
|
||||
<label class="control-label required" for="id_dest_name" translate>
|
||||
Destination Object
|
||||
</label>
|
||||
<span class="hz-icon-required fa fa-asterisk"></span>
|
||||
<div>
|
||||
<input class="form-control"
|
||||
id="id_dest_name"
|
||||
name="dest_name"
|
||||
type="text"
|
||||
maxlength="255"
|
||||
ng-model="ctrl.model.dest_name"
|
||||
ng-required="true"
|
||||
ng-model-options="{ debounce: 1000 }"
|
||||
check-copy-destination
|
||||
>
|
||||
</div>
|
||||
<span class="help-block"
|
||||
ng-show="copyForm.dest_name.$error.required && copyForm.dest_name.$dirty"
|
||||
translate>
|
||||
This field is required.
|
||||
</span>
|
||||
<span class="help-block text-danger"
|
||||
ng-show="copyForm.dest_name.$error.objectExists"
|
||||
translate>
|
||||
This name already exists.
|
||||
</span>
|
||||
</div>
|
||||
</fieldset>
|
||||
</div>
|
||||
|
||||
<div class="col-sm-6">
|
||||
<p translate>
|
||||
You can copy objects. You have to create destination container prior to copy.
|
||||
</p>
|
||||
<p translate>
|
||||
You can specify folder by using '/' at destination object field.
|
||||
For example, if you want to copy object under the folder named 'folder1', you need to specify destination object like 'folder1/[your object name]'.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="modal-footer">
|
||||
<button class="btn btn-default" ng-click="$dismiss()">
|
||||
<span class="fa fa-close"></span>
|
||||
<translate>Cancel</translate>
|
||||
</button>
|
||||
<button class="btn btn-primary" ng-click="$close(ctrl.model)"
|
||||
ng-disabled="copyForm.$invalid">
|
||||
<span class="fa fa-upload"></span>
|
||||
<translate>Copy Object</translate>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
@ -23,6 +23,7 @@
|
||||
.factory('horizon.dashboard.project.containers.objects-actions.download', downloadService)
|
||||
.factory('horizon.dashboard.project.containers.objects-actions.edit', editService)
|
||||
.factory('horizon.dashboard.project.containers.objects-actions.view', viewService)
|
||||
.factory('horizon.dashboard.project.containers.objects-actions.copy', copyService)
|
||||
.run(registerActions);
|
||||
|
||||
registerActions.$inject = [
|
||||
@ -32,6 +33,7 @@
|
||||
'horizon.dashboard.project.containers.objects-actions.download',
|
||||
'horizon.dashboard.project.containers.objects-actions.edit',
|
||||
'horizon.dashboard.project.containers.objects-actions.view',
|
||||
'horizon.dashboard.project.containers.objects-actions.copy',
|
||||
'horizon.framework.util.i18n.gettext'
|
||||
];
|
||||
/**
|
||||
@ -45,6 +47,7 @@
|
||||
downloadService,
|
||||
editService,
|
||||
viewService,
|
||||
copyService,
|
||||
gettext
|
||||
) {
|
||||
registryService.getResourceType(objectResCode).itemActions
|
||||
@ -60,6 +63,10 @@
|
||||
service: editService,
|
||||
template: {text: gettext('Edit')}
|
||||
})
|
||||
.append({
|
||||
service: copyService,
|
||||
template: {text: gettext('Copy')}
|
||||
})
|
||||
.append({
|
||||
service: deleteService,
|
||||
template: {text: gettext('Delete'), type: 'delete'}
|
||||
@ -229,4 +236,93 @@
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
copyService.$inject = [
|
||||
'horizon.app.core.openstack-service-api.swift',
|
||||
'horizon.dashboard.project.containers.basePath',
|
||||
'horizon.dashboard.project.containers.containerRoute',
|
||||
'horizon.dashboard.project.containers.containers-model',
|
||||
'horizon.framework.util.q.extensions',
|
||||
'horizon.framework.widgets.modal-wait-spinner.service',
|
||||
'horizon.framework.widgets.toast.service',
|
||||
'$uibModal',
|
||||
'$location'
|
||||
];
|
||||
|
||||
function copyService(swiftAPI,
|
||||
basePath,
|
||||
containerRoute,
|
||||
model,
|
||||
$qExtensions,
|
||||
modalWaitSpinnerService,
|
||||
toastService,
|
||||
$uibModal,
|
||||
$location) {
|
||||
return {
|
||||
allowed: allowed,
|
||||
perform: perform
|
||||
};
|
||||
|
||||
function allowed(file) {
|
||||
var objectCheck = file.is_object;
|
||||
var capacityCheck = (file.bytes > 0);
|
||||
var result = (objectCheck && capacityCheck);
|
||||
return $qExtensions.booleanAsPromise(result);
|
||||
}
|
||||
|
||||
function perform(file) {
|
||||
var localSpec = {
|
||||
backdrop: 'static',
|
||||
keyboard: false,
|
||||
controller: 'horizon.dashboard.project.containers.CopyObjectModalController as ctrl',
|
||||
templateUrl: basePath + 'copy-object-modal.html',
|
||||
resolve: {
|
||||
fileDetails: function fileDetails() {
|
||||
return {
|
||||
path: file.path,
|
||||
container: model.container.name
|
||||
};
|
||||
}
|
||||
}
|
||||
};
|
||||
return $uibModal.open(localSpec).result.then(copyObjectCallback);
|
||||
}
|
||||
|
||||
function copyObjectCallback(copyInfo) {
|
||||
|
||||
modalWaitSpinnerService.showModalSpinner(gettext("Copying"));
|
||||
swiftAPI.copyObject(
|
||||
model.container.name,
|
||||
copyInfo.path,
|
||||
copyInfo.dest_container,
|
||||
copyInfo.dest_name
|
||||
).then(success, error);
|
||||
|
||||
function success() {
|
||||
var dstNameArray = copyInfo.dest_name.split('/');
|
||||
dstNameArray.pop();
|
||||
var dstFolder = dstNameArray.join('/');
|
||||
|
||||
modalWaitSpinnerService.hideModalSpinner();
|
||||
toastService.add(
|
||||
'success',
|
||||
interpolate(gettext('Object %(path)s has copied.'), copyInfo, true)
|
||||
);
|
||||
|
||||
model.updateContainer();
|
||||
model.selectContainer(
|
||||
copyInfo.dest_container,
|
||||
dstFolder
|
||||
).then(function openDest() {
|
||||
var path = containerRoute + copyInfo.dest_container + '/' + dstFolder;
|
||||
$location.path(path);
|
||||
});
|
||||
}
|
||||
|
||||
function error() {
|
||||
modalWaitSpinnerService.hideModalSpinner();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
})();
|
||||
|
@ -41,7 +41,7 @@
|
||||
}));
|
||||
|
||||
it('should create an actions list', function test() {
|
||||
expect(rowActions.length).toEqual(4);
|
||||
expect(rowActions.length).toEqual(5);
|
||||
angular.forEach(rowActions, function check(action) {
|
||||
expect(action.service).toBeDefined();
|
||||
expect(action.template).toBeDefined();
|
||||
@ -262,6 +262,123 @@
|
||||
});
|
||||
});
|
||||
|
||||
describe('copyService', function test() {
|
||||
var swiftAPI, copyService, modalWaitSpinnerService, toastService, $q, objectDef;
|
||||
|
||||
beforeEach(inject(function inject($injector, _$q_) {
|
||||
swiftAPI = $injector.get('horizon.app.core.openstack-service-api.swift');
|
||||
copyService = $injector.get('horizon.dashboard.project.containers.objects-actions.copy');
|
||||
modalWaitSpinnerService = $injector.get(
|
||||
'horizon.framework.widgets.modal-wait-spinner.service'
|
||||
);
|
||||
toastService = $injector.get('horizon.framework.widgets.toast.service');
|
||||
$q = _$q_;
|
||||
}));
|
||||
|
||||
it('should have an allowed and perform', function test() {
|
||||
expect(copyService.allowed).toBeDefined();
|
||||
expect(copyService.perform).toBeDefined();
|
||||
});
|
||||
|
||||
it('should only allow files which has size(bytes) over 0', function test() {
|
||||
objectDef = {
|
||||
is_object: true,
|
||||
bytes: 1
|
||||
};
|
||||
expectAllowed(copyService.allowed(objectDef));
|
||||
});
|
||||
|
||||
it('should not allow folders', function test() {
|
||||
objectDef = {
|
||||
is_object: false,
|
||||
bytes: 1
|
||||
};
|
||||
expectNotAllowed(copyService.allowed(objectDef));
|
||||
});
|
||||
|
||||
it('should not allow 0 byte file, because it means separated files', function test() {
|
||||
objectDef = {
|
||||
is_object: true,
|
||||
bytes: 0
|
||||
};
|
||||
expectNotAllowed(copyService.allowed(objectDef));
|
||||
});
|
||||
|
||||
it('should handle copy success correctly', function() {
|
||||
var modalDeferred = $q.defer();
|
||||
var apiDeferred = $q.defer();
|
||||
var result = { result: modalDeferred.promise };
|
||||
spyOn($uibModal, 'open').and.returnValue(result);
|
||||
spyOn(modalWaitSpinnerService, 'showModalSpinner');
|
||||
spyOn(modalWaitSpinnerService, 'hideModalSpinner');
|
||||
spyOn(swiftAPI, 'copyObject').and.returnValue(apiDeferred.promise);
|
||||
spyOn(toastService, 'add').and.callThrough();
|
||||
spyOn(model,'updateContainer');
|
||||
spyOn(model,'selectContainer').and.returnValue(apiDeferred.promise);
|
||||
|
||||
copyService.perform();
|
||||
model.container = {name: 'spam'};
|
||||
$rootScope.$apply();
|
||||
|
||||
// Close the modal, make sure API call succeeds
|
||||
var sourceObjectPath = 'sourceObjectPath';
|
||||
modalDeferred.resolve({name: 'ham',
|
||||
path: sourceObjectPath,
|
||||
dest_container: 'dest_container',
|
||||
dest_name: 'dest_name'});
|
||||
apiDeferred.resolve();
|
||||
$rootScope.$apply();
|
||||
|
||||
// Check the string of functions called by this code path succeed
|
||||
expect($uibModal.open).toHaveBeenCalled();
|
||||
expect(modalWaitSpinnerService.showModalSpinner).toHaveBeenCalled();
|
||||
expect(swiftAPI.copyObject).toHaveBeenCalled();
|
||||
expect(toastService.add).
|
||||
toHaveBeenCalledWith('success', 'Object ' + sourceObjectPath +
|
||||
' has copied.');
|
||||
expect(modalWaitSpinnerService.hideModalSpinner).toHaveBeenCalled();
|
||||
expect(model.updateContainer).toHaveBeenCalled();
|
||||
expect(model.selectContainer).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should handle copy error correctly', function() {
|
||||
var modalDeferred = $q.defer();
|
||||
var apiDeferred = $q.defer();
|
||||
var result = { result: modalDeferred.promise };
|
||||
spyOn($uibModal, 'open').and.returnValue(result);
|
||||
spyOn(modalWaitSpinnerService, 'showModalSpinner');
|
||||
spyOn(modalWaitSpinnerService, 'hideModalSpinner');
|
||||
spyOn(swiftAPI, 'copyObject').and.returnValue(apiDeferred.promise);
|
||||
spyOn(toastService, 'add').and.callThrough();
|
||||
spyOn(model,'updateContainer');
|
||||
spyOn(model,'selectContainer').and.returnValue(apiDeferred.promise);
|
||||
|
||||
copyService.perform();
|
||||
model.container = {name: 'spam'};
|
||||
$rootScope.$apply();
|
||||
|
||||
// Close the modal, make sure API call succeeds
|
||||
var sourceObjectPath = 'sourceObjectPath';
|
||||
modalDeferred.resolve({name: 'ham',
|
||||
path: sourceObjectPath,
|
||||
dest_container: 'dest_container',
|
||||
dest_name: 'dest_name'});
|
||||
apiDeferred.reject();
|
||||
$rootScope.$apply();
|
||||
|
||||
// Check the string of functions called by this code path succeed
|
||||
expect(modalWaitSpinnerService.showModalSpinner).toHaveBeenCalled();
|
||||
expect(swiftAPI.copyObject).toHaveBeenCalled();
|
||||
expect(modalWaitSpinnerService.hideModalSpinner).toHaveBeenCalled();
|
||||
expect($uibModal.open).toHaveBeenCalled();
|
||||
|
||||
// Check the success branch is not called
|
||||
expect(model.updateContainer).not.toHaveBeenCalled();
|
||||
expect(model.selectContainer).not.toHaveBeenCalled();
|
||||
expect(toastService.add).not.toHaveBeenCalledWith('success');
|
||||
});
|
||||
});
|
||||
|
||||
function exerciseAllowedPromise(promise) {
|
||||
var handler = jasmine.createSpyObj('handler', ['success', 'error']);
|
||||
promise.then(handler.success, handler.error);
|
||||
|
@ -0,0 +1,8 @@
|
||||
---
|
||||
features:
|
||||
- |
|
||||
Added support for Swift object copy as one of row actions.
|
||||
Destination container must exist in advance.
|
||||
To avoid overwriting an existing object,
|
||||
you cannot copy an object if a specified destination object
|
||||
already exists.
|
Loading…
Reference in New Issue
Block a user