337 lines
12 KiB
TypeScript
Raw Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

// (C) Copyright 2015 Martin Dougiamas
//
// 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 { Injectable } from '@angular/core';
import { FileTransfer, FileTransferObject, FileUploadResult, FileTransferError } from '@ionic-native/file-transfer';
import { CoreAppProvider } from '../../../providers/app';
import { CoreFileProvider } from '../../../providers/file';
/**
* Mock the File Transfer Error.
*/
export class FileTransferErrorMock implements FileTransferError {
public static FILE_NOT_FOUND_ERR = 1;
public static INVALID_URL_ERR = 2;
public static CONNECTION_ERR = 3;
public static ABORT_ERR = 4;
public static NOT_MODIFIED_ERR = 5;
constructor(public code: number, public source: string, public target: string, public http_status: number,
public body: string, public exception: string) {
}
};
/**
* Emulates the Cordova FileTransfer plugin in desktop apps and in browser.
*/
@Injectable()
export class FileTransferMock extends FileTransfer {
constructor(private appProvider: CoreAppProvider, private fileProvider: CoreFileProvider) {
super();
}
/**
* Creates a new FileTransferObjectMock object.
*
* @return {FileTransferObjectMock}
*/
create(): FileTransferObjectMock {
return new FileTransferObjectMock(this.appProvider, this.fileProvider);
}
}
/**
* Emulates the FileTransferObject class in desktop apps and in browser.
*/
export class FileTransferObjectMock extends FileTransferObject {
progressListener: (event: ProgressEvent) => any;
source: string;
target: string;
xhr: XMLHttpRequest;
private reject: Function;
constructor(private appProvider: CoreAppProvider, private fileProvider: CoreFileProvider) {
super();
}
/**
* Aborts an in-progress transfer. The onerror callback is passed a FileTransferError
* object which has an error code of FileTransferError.ABORT_ERR.
*/
abort(): void {
if (this.xhr) {
this.xhr.abort();
this.reject(new FileTransferErrorMock(FileTransferErrorMock.ABORT_ERR, this.source, this.target, null, null, null));
}
}
/**
* Downloads a file from server.
*
* @param {string} source URL of the server to download the file, as encoded by encodeURI().
* @param {string} target Filesystem url representing the file on the device.
* @param {boolean} [trustAllHosts] If set to true, it accepts all security certificates.
* @param {object} [options] Optional parameters, currently only supports headers.
* @returns {Promise<any>} Returns a Promise that resolves to a FileEntry object.
*/
download(source: string, target: string, trustAllHosts?: boolean, options?: { [s: string]: any; }): Promise<any> {
return new Promise((resolve, reject) => {
// Use XMLHttpRequest instead of HttpClient to support onprogress and abort.
let basicAuthHeader = this.getBasicAuthHeader(source),
xhr = new XMLHttpRequest(),
isDesktop = this.appProvider.isDesktop(),
headers = null;
this.xhr = xhr;
this.source = source;
this.target = target;
this.reject = reject;
if (basicAuthHeader) {
source = source.replace(this.getUrlCredentials(source) + '@', '');
options = options || {};
options.headers = options.headers || {};
options.headers[basicAuthHeader.name] = basicAuthHeader.value;
}
if (options) {
headers = options.headers || null;
}
// Prepare the request.
xhr.open('GET', source, true);
xhr.responseType = isDesktop ? 'arraybuffer' : 'blob';
for (let name in headers) {
xhr.setRequestHeader(name, headers[name]);
}
(<any>xhr).onprogress = (xhr, ev) => {
if (this.progressListener) {
this.progressListener(ev);
}
};
xhr.onerror = (err) => {
reject(new FileTransferError(-1, source, target, xhr.status, xhr.statusText));
};
xhr.onload = () => {
// Finished dowloading the file.
let response = xhr.response;
if (!response) {
reject();
} else {
const basePath = this.fileProvider.getBasePathInstant();
target = target.replace(basePath, ''); // Remove basePath from the target.
target = target.replace(/%20/g, ' '); // Replace all %20 with spaces.
if (isDesktop) {
// In desktop we need to convert the arraybuffer into a Buffer.
response = Buffer.from(<any> new Uint8Array(response));
}
this.fileProvider.writeFile(target, response).then(resolve, reject);
}
};
xhr.send();
});
}
/**
* Given a URL, check if it has a credentials in it and, if so, return them in a header object.
* This code is extracted from Cordova FileTransfer plugin.
*
* @param {string} urlString The URL to get the credentials from.
* @return {any} The header with the credentials, null if no credentials.
*/
protected getBasicAuthHeader(urlString: string): any {
let header = null;
// This is changed due to MS Windows doesn't support credentials in http uris
// so we detect them by regexp and strip off from result url.
if (window.btoa) {
const credentials = this.getUrlCredentials(urlString);
if (credentials) {
const authHeader = 'Authorization',
authHeaderValue = 'Basic ' + window.btoa(credentials);
header = {
name: authHeader,
value: authHeaderValue
};
}
}
return header;
}
/**
* Given an instance of XMLHttpRequest, get the response headers as an object.
*
* @param {XMLHttpRequest} xhr XMLHttpRequest instance.
* @return {{[s: string]: any}} Object with the headers.
*/
protected getHeadersAsObject(xhr: XMLHttpRequest) : { [s: string]: any } {
const headersString = xhr.getAllResponseHeaders();
let result = {};
if (headersString) {
const headers = headersString.split('\n');
for (let i in headers) {
const headerString = headers[i],
separatorPos = headerString.indexOf(':');
if (separatorPos != -1) {
result[headerString.substr(0, separatorPos)] = headerString.substr(separatorPos + 1).trim();
}
}
}
return result;
}
/**
* Get the credentials from a URL.
* This code is extracted from Cordova FileTransfer plugin.
*
* @param {string} urlString The URL to get the credentials from.
* @return {string} Retrieved credentials.
*/
protected getUrlCredentials(urlString: string) : string {
const credentialsPattern = /^https?\:\/\/(?:(?:(([^:@\/]*)(?::([^@\/]*))?)?@)?([^:\/?#]*)(?::(\d*))?).*$/,
credentials = credentialsPattern.exec(urlString);
return credentials && credentials[1];
}
/**
* Registers a listener that gets called whenever a new chunk of data is transferred.
*
* @param {Function} listener Listener that takes a progress event.
*/
onProgress(listener: (event: ProgressEvent) => any): void {
this.progressListener = listener;
}
/**
* Sends a file to a server.
*
* @param {string} fileUrl Filesystem URL representing the file on the device or a data URI.
* @param {string} url URL of the server to receive the file, as encoded by encodeURI().
* @param {FileUploadOptions} [options] Optional parameters.
* @param {boolean} [trustAllHosts] If set to true, it accepts all security certificates.
* @returns {Promise<FileUploadResult>} Promise that resolves to a FileUploadResult and rejects with FileTransferError.
*/
upload(fileUrl: string, url: string, options?: FileUploadOptions, trustAllHosts?: boolean): Promise<FileUploadResult> {
return new Promise((resolve, reject) => {
let fileKey = null,
fileName = null,
mimeType = null,
params = null,
headers = null,
httpMethod = null,
basicAuthHeader = this.getBasicAuthHeader(url);
if (basicAuthHeader) {
url = url.replace(this.getUrlCredentials(url) + '@', '');
options = options || {};
options.headers = options.headers || {};
options.headers[basicAuthHeader.name] = basicAuthHeader.value;
}
if (options) {
fileKey = options.fileKey;
fileName = options.fileName;
mimeType = options.mimeType;
headers = options.headers;
httpMethod = options.httpMethod || 'POST';
if (httpMethod.toUpperCase() == "PUT"){
httpMethod = 'PUT';
} else {
httpMethod = 'POST';
}
if (options.params) {
params = options.params;
} else {
params = {};
}
}
// Add fileKey and fileName to the headers.
headers = headers || {};
if (!headers['Content-Disposition']) {
headers['Content-Disposition'] = 'form-data;' + (fileKey ? ' name="' + fileKey + '";' : '') +
(fileName ? ' filename="' + fileName + '"' : '')
}
// For some reason, adding a Content-Type header with the mimeType makes the request fail (it doesn't detect
// the token in the params). Don't include this header, and delete it if it's supplied.
delete headers['Content-Type'];
// Get the file to upload.
this.fileProvider.getFile(fileUrl).then((fileEntry) => {
return this.fileProvider.getFileObjectFromFileEntry(fileEntry);
}).then((file) => {
// Use XMLHttpRequest instead of HttpClient to support onprogress and abort.
let xhr = new XMLHttpRequest();
xhr.open(httpMethod || 'POST', url);
for (let name in headers) {
// Filter "unsafe" headers.
if (name != 'Connection') {
xhr.setRequestHeader(name, headers[name]);
}
}
(<any>xhr).onprogress = (xhr, ev) => {
if (this.progressListener) {
this.progressListener(ev);
}
};
this.xhr = xhr;
this.source = fileUrl;
this.target = url;
this.reject = reject;
xhr.onerror = () => {
reject(new FileTransferError(-1, fileUrl, url, xhr.status, xhr.statusText));
};
xhr.onload = () => {
// Finished uploading the file.
let result: FileUploadResult = {
bytesSent: file.size,
responseCode: xhr.status,
response: xhr.response,
headers: this.getHeadersAsObject(xhr)
};
resolve(result);
};
// Create a form data to send params and the file.
let fd = new FormData();
for (var name in params) {
fd.append(name, params[name]);
}
fd.append('file', file);
xhr.send(fd);
}).catch(reject);
});
}
}