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,
|
headers = {"X-Copy-From": FOLDER_DELIMITER.join([orig_container_name,
|
||||||
orig_object_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,
|
new_object_name,
|
||||||
None,
|
None,
|
||||||
headers=headers)
|
headers=headers)
|
||||||
|
|
||||||
|
obj_info = {'name': new_object_name, 'etag': etag}
|
||||||
|
return StorageObject(obj_info, new_container_name)
|
||||||
|
|
||||||
|
|
||||||
@profiler.trace
|
@profiler.trace
|
||||||
def swift_upload_object(request, container_name, object_name,
|
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.download', downloadService)
|
||||||
.factory('horizon.dashboard.project.containers.objects-actions.edit', editService)
|
.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.view', viewService)
|
||||||
|
.factory('horizon.dashboard.project.containers.objects-actions.copy', copyService)
|
||||||
.run(registerActions);
|
.run(registerActions);
|
||||||
|
|
||||||
registerActions.$inject = [
|
registerActions.$inject = [
|
||||||
@ -32,6 +33,7 @@
|
|||||||
'horizon.dashboard.project.containers.objects-actions.download',
|
'horizon.dashboard.project.containers.objects-actions.download',
|
||||||
'horizon.dashboard.project.containers.objects-actions.edit',
|
'horizon.dashboard.project.containers.objects-actions.edit',
|
||||||
'horizon.dashboard.project.containers.objects-actions.view',
|
'horizon.dashboard.project.containers.objects-actions.view',
|
||||||
|
'horizon.dashboard.project.containers.objects-actions.copy',
|
||||||
'horizon.framework.util.i18n.gettext'
|
'horizon.framework.util.i18n.gettext'
|
||||||
];
|
];
|
||||||
/**
|
/**
|
||||||
@ -45,6 +47,7 @@
|
|||||||
downloadService,
|
downloadService,
|
||||||
editService,
|
editService,
|
||||||
viewService,
|
viewService,
|
||||||
|
copyService,
|
||||||
gettext
|
gettext
|
||||||
) {
|
) {
|
||||||
registryService.getResourceType(objectResCode).itemActions
|
registryService.getResourceType(objectResCode).itemActions
|
||||||
@ -60,6 +63,10 @@
|
|||||||
service: editService,
|
service: editService,
|
||||||
template: {text: gettext('Edit')}
|
template: {text: gettext('Edit')}
|
||||||
})
|
})
|
||||||
|
.append({
|
||||||
|
service: copyService,
|
||||||
|
template: {text: gettext('Copy')}
|
||||||
|
})
|
||||||
.append({
|
.append({
|
||||||
service: deleteService,
|
service: deleteService,
|
||||||
template: {text: gettext('Delete'), type: 'delete'}
|
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() {
|
it('should create an actions list', function test() {
|
||||||
expect(rowActions.length).toEqual(4);
|
expect(rowActions.length).toEqual(5);
|
||||||
angular.forEach(rowActions, function check(action) {
|
angular.forEach(rowActions, function check(action) {
|
||||||
expect(action.service).toBeDefined();
|
expect(action.service).toBeDefined();
|
||||||
expect(action.template).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) {
|
function exerciseAllowedPromise(promise) {
|
||||||
var handler = jasmine.createSpyObj('handler', ['success', 'error']);
|
var handler = jasmine.createSpyObj('handler', ['success', 'error']);
|
||||||
promise.then(handler.success, handler.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