[ADF-1002] Move the UploadService from the upload to core component and share it with the Process service (#2055)

* use the same upload component between the content and process service

* Create a BaseUploadServe inside the core
The AlfrescoUpload should extend the BaseUpload

* Improve the code

* Move the process service from the demo shell to form component

* Merge [ADF-917] added exlcluded files into app config to prevent special files

* Remove typo

* Fix import

* Fix FileModel import

* Fix Denys comment

* Fix unit test with the new path of UploadService

* Add missing implementation
This commit is contained in:
Maurizio Vitale
2017-07-07 15:58:59 +01:00
committed by Eugenio Romano
parent 945dac6c49
commit f891c11e78
24 changed files with 141 additions and 57 deletions

View File

@@ -0,0 +1,36 @@
/*!
* @license
* Copyright 2016 Alfresco Software, Ltd.
*
* 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.
*/
import { FileModel, FileUploadStatus } from '../models/file.model';
export class FileUploadEvent {
constructor(
public readonly file: FileModel,
public readonly status: FileUploadStatus = FileUploadStatus.Pending,
public readonly error: any = null) {
}
}
export class FileUploadCompleteEvent extends FileUploadEvent {
constructor(file: FileModel, public totalComplete: number = 0, public data?: any, public totalAborted: number = 0) {
super(file, FileUploadStatus.Complete);
}
}

View File

@@ -0,0 +1,74 @@
/*!
* @license
* Copyright 2016 Alfresco Software, Ltd.
*
* 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.
*/
export interface FileUploadProgress {
loaded: number;
total: number;
percent: number;
}
export interface FileUploadOptions {
newVersion?: boolean;
parentId?: string;
path?: string;
}
export enum FileUploadStatus {
Pending = 0,
Complete = 1,
Starting = 2,
Progress = 3,
Cancelled = 4,
Aborted = 5,
Error = 6
}
export class FileModel {
readonly id: string;
readonly name: string;
readonly size: number;
readonly file: File;
status: FileUploadStatus = FileUploadStatus.Pending;
progress: FileUploadProgress;
options: FileUploadOptions;
constructor(file: File, options?: FileUploadOptions) {
this.file = file;
this.id = this.generateId();
this.name = file.name;
this.size = file.size;
this.progress = {
loaded: 0,
total: 0,
percent: 0
};
this.options = Object.assign({}, {
newVersion: false
}, options);
}
private generateId(): string {
return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function (c) {
let r = Math.random() * 16 | 0, v = c === 'x' ? r : (r & 0x3 | 0x8);
return v.toString(16);
});
}
}

View File

@@ -16,3 +16,4 @@
*/
export * from './card-view.model';
export * from './file.model';

View File

@@ -32,3 +32,4 @@ export * from './alfresco-translation.service';
export * from './alfresco-translate-loader.service';
export * from './app-config.service';
export * from './thumbnail.service';
export * from './upload.service';

View File

@@ -0,0 +1,209 @@
/*!
* @license
* Copyright 2016 Alfresco Software, Ltd.
*
* 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.
*/
import { EventEmitter } from '@angular/core';
import { TestBed } from '@angular/core/testing';
import { AppConfigModule, CoreModule, FileModel, FileUploadOptions } from 'ng2-alfresco-core';
import { UploadService } from './upload.service';
declare let jasmine: any;
describe('UploadService', () => {
let service: UploadService;
beforeEach(() => {
TestBed.configureTestingModule({
imports: [
CoreModule.forRoot(),
AppConfigModule.forRoot('app.config.json', {
ecmHost: 'http://localhost:9876/ecm',
files: {
excluded: ['.DS_Store', 'desktop.ini', '.git', '*.git']
}
})
],
providers: [
UploadService
]
});
service = TestBed.get(UploadService);
jasmine.Ajax.install();
});
afterEach(() => {
jasmine.Ajax.uninstall();
});
it('should return an empty queue if no elements are added', () => {
expect(service.getQueue().length).toEqual(0);
});
it('should add an element in the queue and returns it', () => {
let filesFake = new FileModel(<File>{ name: 'fake-name', size: 10 });
service.addToQueue(filesFake);
expect(service.getQueue().length).toEqual(1);
});
it('should add two elements in the queue and returns them', () => {
let filesFake = [
new FileModel(<File>{ name: 'fake-name', size: 10 }),
new FileModel(<File>{ name: 'fake-name2', size: 20 })
];
service.addToQueue(...filesFake);
expect(service.getQueue().length).toEqual(2);
});
it('should skip hidden macOS files', () => {
const file1 = new FileModel(new File([''], '.git'));
const file2 = new FileModel(new File([''], 'readme.md'));
const result = service.addToQueue(file1, file2);
expect(result.length).toBe(1);
expect(result[0]).toBe(file2);
});
it('should make XHR done request after the file is added in the queue', (done) => {
let emitter = new EventEmitter();
emitter.subscribe(e => {
expect(e.value).toBe('File uploaded');
done();
});
let fileFake = new FileModel(
<File>{ name: 'fake-name', size: 10 },
<FileUploadOptions>{ parentId: '-root-', path: 'fake-dir' }
);
service.addToQueue(fileFake);
service.uploadFilesInTheQueue(emitter);
let request = jasmine.Ajax.requests.mostRecent();
expect(request.url).toBe('http://localhost:9876/ecm/alfresco/api/-default-/public/alfresco/versions/1/nodes/-root-/children?autoRename=true');
expect(request.method).toBe('POST');
jasmine.Ajax.requests.mostRecent().respondWith({
'status': 200,
contentType: 'text/plain',
responseText: 'File uploaded'
});
});
it('should make XHR error request after an error occur', (done) => {
let emitter = new EventEmitter();
emitter.subscribe(e => {
expect(e.value).toBe('Error file uploaded');
done();
});
let fileFake = new FileModel(
<File>{ name: 'fake-name', size: 10 },
<FileUploadOptions>{ parentId: '-root-' }
);
service.addToQueue(fileFake);
service.uploadFilesInTheQueue(emitter);
expect(jasmine.Ajax.requests.mostRecent().url)
.toBe('http://localhost:9876/ecm/alfresco/api/-default-/public/alfresco/versions/1/nodes/-root-/children?autoRename=true');
jasmine.Ajax.requests.mostRecent().respondWith({
'status': 404,
contentType: 'text/plain',
responseText: 'Error file uploaded'
});
});
it('should make XHR abort request after the xhr abort is called', (done) => {
let emitter = new EventEmitter();
emitter.subscribe(e => {
expect(e.value).toEqual('File aborted');
done();
});
let fileFake = new FileModel(<File>{ name: 'fake-name', size: 10 });
service.addToQueue(fileFake);
service.uploadFilesInTheQueue(emitter);
let file = service.getQueue();
service.cancelUpload(...file);
});
it('If versioning is true autoRename should not be present and majorVersion should be a param', () => {
let emitter = new EventEmitter();
const filesFake = new FileModel(<File>{ name: 'fake-name', size: 10 }, { newVersion: true });
service.addToQueue(filesFake);
service.uploadFilesInTheQueue(emitter);
expect(jasmine.Ajax.requests.mostRecent().url.endsWith('autoRename=true')).toBe(false);
expect(jasmine.Ajax.requests.mostRecent().params.has('majorVersion')).toBe(true);
});
it('should use custom root folder ID given to the service', (done) => {
let emitter = new EventEmitter();
emitter.subscribe(e => {
expect(e.value).toBe('File uploaded');
done();
});
let filesFake = new FileModel(
<File>{ name: 'fake-name', size: 10 },
<FileUploadOptions> { parentId: '123', path: 'fake-dir' }
);
service.addToQueue(filesFake);
service.uploadFilesInTheQueue(emitter);
let request = jasmine.Ajax.requests.mostRecent();
expect(request.url).toBe('http://localhost:9876/ecm/alfresco/api/-default-/public/alfresco/versions/1/nodes/123/children?autoRename=true');
expect(request.method).toBe('POST');
jasmine.Ajax.requests.mostRecent().respondWith({
'status': 200,
contentType: 'text/plain',
responseText: 'File uploaded'
});
});
it('should start downloading the next one if a file of the list is aborted', (done) => {
let emitter = new EventEmitter();
service.fileUploadAborted.subscribe(e => {
expect(e).not.toBeNull();
});
service.fileUploadCancelled.subscribe(e => {
expect(e).not.toBeNull();
done();
});
let fileFake1 = new FileModel(<File>{ name: 'fake-name1', size: 10 });
let fileFake2 = new FileModel(<File>{ name: 'fake-name2', size: 10 });
let filelist = [fileFake1, fileFake2];
service.addToQueue(...filelist);
service.uploadFilesInTheQueue(emitter);
let file = service.getQueue();
service.cancelUpload(...file);
});
it('should remove from the queue all the files in the exluded list', () => {
const file1 = new FileModel(new File([''], '.git'));
const file2 = new FileModel(new File([''], '.DS_Store'));
const file3 = new FileModel(new File([''], 'desktop.ini'));
const file4 = new FileModel(new File([''], 'readme.md'));
const file5 = new FileModel(new File([''], 'test.git'));
const result = service.addToQueue(file1, file2, file3, file4, file5);
expect(result.length).toBe(1);
expect(result[0]).toBe(file4);
});
});

View File

@@ -0,0 +1,259 @@
/*!
* @license
* Copyright 2016 Alfresco Software, Ltd.
*
* 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.
*/
import { EventEmitter, Injectable } from '@angular/core';
import * as minimatch from 'minimatch';
import { Subject } from 'rxjs/Rx';
import { FileUploadCompleteEvent, FileUploadEvent } from '../events/file.event';
import { FileModel, FileUploadProgress, FileUploadStatus } from '../models/file.model';
import { AlfrescoApiService } from './alfresco-api.service';
import { AppConfigService } from './app-config.service';
@Injectable()
export class UploadService {
private queue: FileModel[] = [];
private cache: { [key: string]: any } = {};
private totalComplete: number = 0;
private totalAborted: number = 0;
private activeTask: Promise<any> = null;
private excludedFileList: String[] = [];
queueChanged: Subject<FileModel[]> = new Subject<FileModel[]>();
fileUpload: Subject<FileUploadEvent> = new Subject<FileUploadEvent>();
fileUploadStarting: Subject<FileUploadEvent> = new Subject<FileUploadEvent>();
fileUploadCancelled: Subject<FileUploadEvent> = new Subject<FileUploadEvent>();
fileUploadProgress: Subject<FileUploadEvent> = new Subject<FileUploadEvent>();
fileUploadAborted: Subject<FileUploadEvent> = new Subject<FileUploadEvent>();
fileUploadError: Subject<FileUploadEvent> = new Subject<FileUploadEvent>();
fileUploadComplete: Subject<FileUploadCompleteEvent> = new Subject<FileUploadCompleteEvent>();
constructor(private apiService: AlfrescoApiService, private appConfigService: AppConfigService) {
this.excludedFileList = <String[]>this.appConfigService.get('files.excluded');
}
/**
* Checks whether the service is uploading a file.
*
* @returns {boolean}
*
* @memberof UploadService
*/
isUploading(): boolean {
return this.activeTask ? true : false;
}
/**
* Returns the file Queue
*
* @return {FileModel[]} - files in the upload queue.
*/
getQueue(): FileModel[] {
return this.queue;
}
/**
* Add files to the uploading queue to be uploaded.
*
* Examples:
* addToQueue(file); // pass one file
* addToQueue(file1, file2, file3); // pass multiple files
* addToQueue(...[file1, file2, file3]); // pass an array of files
*/
addToQueue(...files: FileModel[]): FileModel[] {
const allowedFiles = files.filter(f => this.filterElement(f));
this.queue = this.queue.concat(allowedFiles);
this.queueChanged.next(this.queue);
return allowedFiles;
}
private filterElement(file: FileModel) {
let isAllowed = true;
if (this.excludedFileList) {
isAllowed = this.excludedFileList.filter(expr => minimatch(file.name, expr)).length === 0;
}
return isAllowed;
}
/**
* Pick all the files in the queue that are not been uploaded yet and upload it into the directory folder.
*
* @param {EventEmitter<any>} emitter @deprecated emitter to invoke on file status change
*
* @memberof UploadService
*/
uploadFilesInTheQueue(emitter: EventEmitter<any>): void {
if (!this.activeTask) {
let file = this.queue.find(f => f.status === FileUploadStatus.Pending);
if (file) {
this.onUploadStarting(file);
const promise = this.beginUpload(file, emitter);
this.activeTask = promise;
this.cache[file.id] = promise;
let next = () => {
this.activeTask = null;
setTimeout(() => this.uploadFilesInTheQueue(emitter), 100);
};
promise.next = next;
promise.then(
() => next(),
() => next()
);
}
}
}
cancelUpload(...files: FileModel[]) {
files.forEach(file => {
file.status = FileUploadStatus.Cancelled;
const promise = this.cache[file.id];
if (promise) {
promise.abort();
delete this.cache[file.id];
}
const event = new FileUploadEvent(file, FileUploadStatus.Cancelled);
this.fileUpload.next(event);
this.fileUploadCancelled.next(event);
});
}
clearQueue() {
this.queue = [];
this.totalComplete = 0;
this.totalAborted = 0;
}
getUploadPromise(file: FileModel) {
let opts: any = {
renditions: 'doclib'
};
if (file.options.newVersion === true) {
opts.overwrite = true;
opts.majorVersion = true;
} else {
opts.autoRename = true;
}
return this.apiService.getInstance().upload.uploadFile(
file.file,
file.options.path,
file.options.parentId,
null,
opts
);
}
private beginUpload(file: FileModel, /* @deprecated */emitter: EventEmitter<any>): any {
let promise = this.getUploadPromise(file);
promise.on('progress', (progress: FileUploadProgress) => {
this.onUploadProgress(file, progress);
})
.on('abort', () => {
this.onUploadAborted(file);
emitter.emit({ value: 'File aborted' });
})
.on('error', err => {
this.onUploadError(file, err);
emitter.emit({ value: 'Error file uploaded' });
})
.on('success', data => {
this.onUploadComplete(file, data);
emitter.emit({ value: data });
})
.catch(err => {
this.onUploadError(file, err);
});
return promise;
}
private onUploadStarting(file: FileModel): void {
if (file) {
file.status = FileUploadStatus.Starting;
const event = new FileUploadEvent(file, FileUploadStatus.Starting);
this.fileUpload.next(event);
this.fileUploadStarting.next(event);
}
}
private onUploadProgress(file: FileModel, progress: FileUploadProgress): void {
if (file) {
file.progress = progress;
file.status = FileUploadStatus.Progress;
const event = new FileUploadEvent(file, FileUploadStatus.Progress);
this.fileUpload.next(event);
this.fileUploadProgress.next(event);
}
}
private onUploadError(file: FileModel, error: any): void {
if (file) {
file.status = FileUploadStatus.Error;
const promise = this.cache[file.id];
if (promise) {
delete this.cache[file.id];
}
const event = new FileUploadEvent(file, FileUploadStatus.Error, error);
this.fileUpload.next(event);
this.fileUploadError.next(event);
}
}
private onUploadComplete(file: FileModel, data: any): void {
if (file) {
file.status = FileUploadStatus.Complete;
this.totalComplete++;
const promise = this.cache[file.id];
if (promise) {
delete this.cache[file.id];
}
const event = new FileUploadCompleteEvent(file, this.totalComplete, data, this.totalAborted);
this.fileUpload.next(event);
this.fileUploadComplete.next(event);
}
}
private onUploadAborted(file: FileModel): void {
if (file) {
file.status = FileUploadStatus.Aborted;
this.totalAborted++;
const promise = this.cache[file.id];
if (promise) {
delete this.cache[file.id];
}
const event = new FileUploadEvent(file, FileUploadStatus.Aborted);
this.fileUpload.next(event);
this.fileUploadAborted.next(event);
promise.next();
}
}
}