390 lines
14 KiB
TypeScript
390 lines
14 KiB
TypeScript
// (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 {
|
|
static FILE_NOT_FOUND_ERR = 1;
|
|
static INVALID_URL_ERR = 2;
|
|
static CONNECTION_ERR = 3;
|
|
static ABORT_ERR = 4;
|
|
static NOT_MODIFIED_ERR = 5;
|
|
|
|
// tslint:disable-next-line: variable-name
|
|
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;
|
|
protected 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): void => {
|
|
// Use XMLHttpRequest instead of HttpClient to support onprogress and abort.
|
|
const basicAuthHeader = this.getBasicAuthHeader(source),
|
|
xhr = new XMLHttpRequest(),
|
|
isDesktop = this.appProvider.isDesktop();
|
|
let 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 (const name in headers) {
|
|
xhr.setRequestHeader(name, headers[name]);
|
|
}
|
|
|
|
(<any> xhr).onprogress = (xhr, ev): void => {
|
|
if (this.progressListener) {
|
|
this.progressListener(ev);
|
|
}
|
|
};
|
|
|
|
xhr.onerror = (err): void => {
|
|
reject(new FileTransferErrorMock(-1, source, target, xhr.status, xhr.statusText, null));
|
|
};
|
|
|
|
xhr.onload = (): void => {
|
|
// Finished dowloading the file.
|
|
let response = xhr.response || xhr.responseText;
|
|
|
|
const status = Math.max(xhr.status === 1223 ? 204 : xhr.status, 0);
|
|
if (status < 200 || status >= 300) {
|
|
// Request failed. Try to get the error message.
|
|
this.parseResponse(response).then((response) => {
|
|
reject(new FileTransferErrorMock(-1, source, target, xhr.status, response || xhr.statusText, null));
|
|
});
|
|
|
|
return;
|
|
}
|
|
|
|
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;
|
|
|
|
// 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(),
|
|
result = {};
|
|
|
|
if (headersString) {
|
|
const headers = headersString.split('\n');
|
|
for (const 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;
|
|
}
|
|
|
|
/**
|
|
* Same as Javascript's JSON.parse, but it will handle errors.
|
|
*
|
|
* @param {string} json JSON text.
|
|
* @return {any} JSON parsed as object or what it gets.
|
|
*/
|
|
protected parseJSON(json: string): any {
|
|
try {
|
|
return JSON.parse(json);
|
|
} catch (ex) {
|
|
// Error.
|
|
}
|
|
|
|
return json;
|
|
}
|
|
|
|
/**
|
|
* Parse a response, converting it into text and the into an object if needed.
|
|
*
|
|
* @param {any} response The response to parse.
|
|
* @return {Promise<any>} Promise resolved with the parsed response.
|
|
*/
|
|
protected parseResponse(response: any): Promise<any> {
|
|
return new Promise((resolve, reject): void => {
|
|
if (!response) {
|
|
resolve('');
|
|
} else if (response.toString && response.toString() == '[object Blob]') {
|
|
// Convert the Blob into text.
|
|
const reader = new FileReader();
|
|
reader.onloadend = (): void => {
|
|
resolve(reader.result);
|
|
};
|
|
reader.readAsText(response);
|
|
|
|
} else if (response.toString && response.toString() == '[object ArrayBuffer]') {
|
|
// Convert the ArrayBuffer into text.
|
|
resolve(String.fromCharCode.apply(null, new Uint8Array(response)));
|
|
} else {
|
|
resolve(response);
|
|
}
|
|
}).then((response: any) => {
|
|
return this.parseJSON(response);
|
|
});
|
|
}
|
|
|
|
/**
|
|
* 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): void => {
|
|
const basicAuthHeader = this.getBasicAuthHeader(url);
|
|
let fileKey = null,
|
|
fileName = null,
|
|
params = null,
|
|
headers = null,
|
|
httpMethod = null;
|
|
|
|
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;
|
|
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 + '"' : '');
|
|
}
|
|
|
|
// 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.
|
|
const xhr = new XMLHttpRequest();
|
|
xhr.open(httpMethod || 'POST', url);
|
|
for (const name in headers) {
|
|
// Filter "unsafe" headers.
|
|
if (name != 'Connection') {
|
|
xhr.setRequestHeader(name, headers[name]);
|
|
}
|
|
}
|
|
|
|
xhr.onprogress = (ev: ProgressEvent): any => {
|
|
if (this.progressListener) {
|
|
this.progressListener(ev);
|
|
}
|
|
};
|
|
|
|
this.xhr = xhr;
|
|
this.source = fileUrl;
|
|
this.target = url;
|
|
this.reject = reject;
|
|
|
|
xhr.onerror = (): void => {
|
|
reject(new FileTransferErrorMock(-1, fileUrl, url, xhr.status, xhr.statusText, null));
|
|
};
|
|
|
|
xhr.onload = (): void => {
|
|
// Finished uploading the file.
|
|
resolve({
|
|
bytesSent: file.size,
|
|
responseCode: xhr.status,
|
|
response: xhr.response,
|
|
headers: this.getHeadersAsObject(xhr)
|
|
});
|
|
};
|
|
|
|
// Create a form data to send params and the file.
|
|
const fd = new FormData();
|
|
for (const name in params) {
|
|
fd.append(name, params[name]);
|
|
}
|
|
fd.append('file', file);
|
|
|
|
xhr.send(fd);
|
|
}).catch(reject);
|
|
});
|
|
}
|
|
}
|