MOBILE-3565 browser: Mock some ionic-native services for browser
parent
459302547b
commit
1ed95a4ade
|
@ -10527,6 +10527,11 @@
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"optional": true
|
"optional": true
|
||||||
},
|
},
|
||||||
|
"immediate": {
|
||||||
|
"version": "3.0.6",
|
||||||
|
"resolved": "https://registry.npmjs.org/immediate/-/immediate-3.0.6.tgz",
|
||||||
|
"integrity": "sha1-nbHb0Pr43m++D13V5Wu2BigN5ps="
|
||||||
|
},
|
||||||
"import-cwd": {
|
"import-cwd": {
|
||||||
"version": "2.1.0",
|
"version": "2.1.0",
|
||||||
"resolved": "https://registry.npmjs.org/import-cwd/-/import-cwd-2.1.0.tgz",
|
"resolved": "https://registry.npmjs.org/import-cwd/-/import-cwd-2.1.0.tgz",
|
||||||
|
@ -11253,8 +11258,7 @@
|
||||||
"isarray": {
|
"isarray": {
|
||||||
"version": "1.0.0",
|
"version": "1.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz",
|
||||||
"integrity": "sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE=",
|
"integrity": "sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE="
|
||||||
"dev": true
|
|
||||||
},
|
},
|
||||||
"isexe": {
|
"isexe": {
|
||||||
"version": "2.0.0",
|
"version": "2.0.0",
|
||||||
|
@ -12559,6 +12563,17 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"jszip": {
|
||||||
|
"version": "3.5.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/jszip/-/jszip-3.5.0.tgz",
|
||||||
|
"integrity": "sha512-WRtu7TPCmYePR1nazfrtuF216cIVon/3GWOvHS9QR5bIwSbnxtdpma6un3jyGGNhHsKCSzn5Ypk+EkDRvTGiFA==",
|
||||||
|
"requires": {
|
||||||
|
"lie": "~3.3.0",
|
||||||
|
"pako": "~1.0.2",
|
||||||
|
"readable-stream": "~2.3.6",
|
||||||
|
"set-immediate-shim": "~1.0.1"
|
||||||
|
}
|
||||||
|
},
|
||||||
"just-debounce": {
|
"just-debounce": {
|
||||||
"version": "1.0.0",
|
"version": "1.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/just-debounce/-/just-debounce-1.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/just-debounce/-/just-debounce-1.0.0.tgz",
|
||||||
|
@ -12741,6 +12756,14 @@
|
||||||
"webpack-sources": "^1.2.0"
|
"webpack-sources": "^1.2.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"lie": {
|
||||||
|
"version": "3.3.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/lie/-/lie-3.3.0.tgz",
|
||||||
|
"integrity": "sha512-UaiMJzeWRlEujzAuw5LokY1L5ecNQYZKfmyZ9L7wDHb/p5etKaxXhohBcrw0EYby+G/NA52vRSN4N39dxHAIwQ==",
|
||||||
|
"requires": {
|
||||||
|
"immediate": "~3.0.5"
|
||||||
|
}
|
||||||
|
},
|
||||||
"liftoff": {
|
"liftoff": {
|
||||||
"version": "3.1.0",
|
"version": "3.1.0",
|
||||||
"resolved": "https://registry.npmjs.org/liftoff/-/liftoff-3.1.0.tgz",
|
"resolved": "https://registry.npmjs.org/liftoff/-/liftoff-3.1.0.tgz",
|
||||||
|
@ -14745,8 +14768,7 @@
|
||||||
"pako": {
|
"pako": {
|
||||||
"version": "1.0.11",
|
"version": "1.0.11",
|
||||||
"resolved": "https://registry.npmjs.org/pako/-/pako-1.0.11.tgz",
|
"resolved": "https://registry.npmjs.org/pako/-/pako-1.0.11.tgz",
|
||||||
"integrity": "sha512-4hLB8Py4zZce5s4yd9XzopqwVv/yGNhV1Bl8NTmCq1763HeK2+EwVTv+leGeL13Dnh2wfbqowVPXCIO0z4taYw==",
|
"integrity": "sha512-4hLB8Py4zZce5s4yd9XzopqwVv/yGNhV1Bl8NTmCq1763HeK2+EwVTv+leGeL13Dnh2wfbqowVPXCIO0z4taYw=="
|
||||||
"dev": true
|
|
||||||
},
|
},
|
||||||
"parallel-transform": {
|
"parallel-transform": {
|
||||||
"version": "1.2.0",
|
"version": "1.2.0",
|
||||||
|
@ -15936,8 +15958,7 @@
|
||||||
"process-nextick-args": {
|
"process-nextick-args": {
|
||||||
"version": "2.0.1",
|
"version": "2.0.1",
|
||||||
"resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz",
|
||||||
"integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==",
|
"integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag=="
|
||||||
"dev": true
|
|
||||||
},
|
},
|
||||||
"progress": {
|
"progress": {
|
||||||
"version": "2.0.3",
|
"version": "2.0.3",
|
||||||
|
@ -16301,7 +16322,6 @@
|
||||||
"version": "2.3.7",
|
"version": "2.3.7",
|
||||||
"resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.7.tgz",
|
"resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.7.tgz",
|
||||||
"integrity": "sha512-Ebho8K4jIbHAxnuxi7o42OrZgF/ZTNcsZj6nRKyUmkhLFq8CHItp/fy6hQZuZmP/n3yZ9VBUbp4zz/mX8hmYPw==",
|
"integrity": "sha512-Ebho8K4jIbHAxnuxi7o42OrZgF/ZTNcsZj6nRKyUmkhLFq8CHItp/fy6hQZuZmP/n3yZ9VBUbp4zz/mX8hmYPw==",
|
||||||
"dev": true,
|
|
||||||
"requires": {
|
"requires": {
|
||||||
"core-util-is": "~1.0.0",
|
"core-util-is": "~1.0.0",
|
||||||
"inherits": "~2.0.3",
|
"inherits": "~2.0.3",
|
||||||
|
@ -17465,6 +17485,11 @@
|
||||||
"integrity": "sha1-BF+XgtARrppoA93TgrJDkrPYkPc=",
|
"integrity": "sha1-BF+XgtARrppoA93TgrJDkrPYkPc=",
|
||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
|
"set-immediate-shim": {
|
||||||
|
"version": "1.0.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/set-immediate-shim/-/set-immediate-shim-1.0.1.tgz",
|
||||||
|
"integrity": "sha1-SysbJ+uAip+NzEgaWOXlb1mfP2E="
|
||||||
|
},
|
||||||
"set-value": {
|
"set-value": {
|
||||||
"version": "2.0.1",
|
"version": "2.0.1",
|
||||||
"resolved": "https://registry.npmjs.org/set-value/-/set-value-2.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/set-value/-/set-value-2.0.1.tgz",
|
||||||
|
@ -18381,7 +18406,6 @@
|
||||||
"version": "1.1.1",
|
"version": "1.1.1",
|
||||||
"resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz",
|
"resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz",
|
||||||
"integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==",
|
"integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==",
|
||||||
"dev": true,
|
|
||||||
"requires": {
|
"requires": {
|
||||||
"safe-buffer": "~5.1.0"
|
"safe-buffer": "~5.1.0"
|
||||||
}
|
}
|
||||||
|
@ -19543,8 +19567,7 @@
|
||||||
"util-deprecate": {
|
"util-deprecate": {
|
||||||
"version": "1.0.2",
|
"version": "1.0.2",
|
||||||
"resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz",
|
"resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz",
|
||||||
"integrity": "sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8=",
|
"integrity": "sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8="
|
||||||
"dev": true
|
|
||||||
},
|
},
|
||||||
"util-promisify": {
|
"util-promisify": {
|
||||||
"version": "2.1.0",
|
"version": "2.1.0",
|
||||||
|
|
|
@ -101,6 +101,7 @@
|
||||||
"cordova-support-google-services": "^1.2.1",
|
"cordova-support-google-services": "^1.2.1",
|
||||||
"cordova.plugins.diagnostic": "^6.0.2",
|
"cordova.plugins.diagnostic": "^6.0.2",
|
||||||
"es6-promise-plugin": "^4.2.2",
|
"es6-promise-plugin": "^4.2.2",
|
||||||
|
"jszip": "^3.5.0",
|
||||||
"moment": "^2.29.0",
|
"moment": "^2.29.0",
|
||||||
"nl.kingsquare.cordova.background-audio": "^1.0.1",
|
"nl.kingsquare.cordova.background-audio": "^1.0.1",
|
||||||
"phonegap-plugin-multidex": "^1.0.0",
|
"phonegap-plugin-multidex": "^1.0.0",
|
||||||
|
|
|
@ -13,6 +13,10 @@
|
||||||
// limitations under the License.
|
// limitations under the License.
|
||||||
|
|
||||||
import { NgModule } from '@angular/core';
|
import { NgModule } from '@angular/core';
|
||||||
|
import { Platform } from '@ionic/angular';
|
||||||
|
|
||||||
|
import { CoreInitDelegate } from '@services/init';
|
||||||
|
import { CoreEmulatorHelperProvider } from './services/helper';
|
||||||
|
|
||||||
// Ionic Native services.
|
// Ionic Native services.
|
||||||
import { Clipboard } from '@ionic-native/clipboard/ngx';
|
import { Clipboard } from '@ionic-native/clipboard/ngx';
|
||||||
|
@ -36,6 +40,16 @@ import { StatusBar } from '@ionic-native/status-bar/ngx';
|
||||||
import { WebIntent } from '@ionic-native/web-intent/ngx';
|
import { WebIntent } from '@ionic-native/web-intent/ngx';
|
||||||
import { Zip } from '@ionic-native/zip/ngx';
|
import { Zip } from '@ionic-native/zip/ngx';
|
||||||
|
|
||||||
|
// Mock services.
|
||||||
|
import { ClipboardMock } from './services/clipboard';
|
||||||
|
import { FileMock } from './services/file';
|
||||||
|
import { FileOpenerMock } from './services/file-opener';
|
||||||
|
import { FileTransferMock } from './services/file-transfer';
|
||||||
|
import { GeolocationMock } from './services/geolocation';
|
||||||
|
import { InAppBrowserMock } from './services/inappbrowser';
|
||||||
|
import { NetworkMock } from './services/network';
|
||||||
|
import { ZipMock } from './services/zip';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* This module handles the emulation of Cordova plugins in browser and desktop.
|
* This module handles the emulation of Cordova plugins in browser and desktop.
|
||||||
*
|
*
|
||||||
|
@ -51,18 +65,47 @@ import { Zip } from '@ionic-native/zip/ngx';
|
||||||
imports: [
|
imports: [
|
||||||
],
|
],
|
||||||
providers: [
|
providers: [
|
||||||
Clipboard,
|
CoreEmulatorHelperProvider,
|
||||||
|
{
|
||||||
|
provide: Clipboard,
|
||||||
|
deps: [Platform], // Use platform instead of AppProvider to prevent errors with singleton injection.
|
||||||
|
useFactory: (platform: Platform): Clipboard => platform.is('cordova') ? new Clipboard() : new ClipboardMock(),
|
||||||
|
},
|
||||||
Device,
|
Device,
|
||||||
Diagnostic,
|
Diagnostic,
|
||||||
File,
|
{
|
||||||
FileOpener,
|
provide: File,
|
||||||
FileTransfer,
|
deps: [Platform],
|
||||||
Geolocation,
|
useFactory: (platform: Platform): File => platform.is('cordova') ? new File() : new FileMock(),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
provide: FileOpener,
|
||||||
|
deps: [Platform],
|
||||||
|
useFactory: (platform: Platform): FileOpener => platform.is('cordova') ? new FileOpener() : new FileOpenerMock(),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
provide: FileTransfer,
|
||||||
|
deps: [Platform],
|
||||||
|
useFactory: (platform: Platform): FileTransfer => platform.is('cordova') ? new FileTransfer() : new FileTransferMock(),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
provide: Geolocation,
|
||||||
|
deps: [Platform],
|
||||||
|
useFactory: (platform: Platform): Geolocation => platform.is('cordova') ? new Geolocation() : new GeolocationMock(),
|
||||||
|
},
|
||||||
HTTP,
|
HTTP,
|
||||||
InAppBrowser,
|
{
|
||||||
|
provide: InAppBrowser,
|
||||||
|
deps: [Platform],
|
||||||
|
useFactory: (platform: Platform): InAppBrowser => platform.is('cordova') ? new InAppBrowser() : new InAppBrowserMock(),
|
||||||
|
},
|
||||||
Keyboard,
|
Keyboard,
|
||||||
LocalNotifications,
|
LocalNotifications,
|
||||||
Network,
|
{
|
||||||
|
provide: Network,
|
||||||
|
deps: [Platform],
|
||||||
|
useFactory: (platform: Platform): Network => platform.is('cordova') ? new Network() : new NetworkMock(),
|
||||||
|
},
|
||||||
Push,
|
Push,
|
||||||
QRScanner,
|
QRScanner,
|
||||||
SplashScreen,
|
SplashScreen,
|
||||||
|
@ -70,7 +113,25 @@ import { Zip } from '@ionic-native/zip/ngx';
|
||||||
StatusBar,
|
StatusBar,
|
||||||
WebIntent,
|
WebIntent,
|
||||||
WebView,
|
WebView,
|
||||||
Zip,
|
{
|
||||||
|
provide: Zip,
|
||||||
|
deps: [Platform, File],
|
||||||
|
useFactory: (platform: Platform, file: File): Zip => platform.is('cordova') ? new Zip() : new ZipMock(file),
|
||||||
|
},
|
||||||
],
|
],
|
||||||
})
|
})
|
||||||
export class CoreEmulatorModule { }
|
export class CoreEmulatorModule {
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
platform: Platform,
|
||||||
|
initDelegate: CoreInitDelegate,
|
||||||
|
helper: CoreEmulatorHelperProvider,
|
||||||
|
) {
|
||||||
|
|
||||||
|
if (!platform.is('cordova')) {
|
||||||
|
// Register an init process to load the Mocks that need it.
|
||||||
|
initDelegate.registerProcess(helper);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
|
@ -0,0 +1,87 @@
|
||||||
|
// (C) Copyright 2015 Moodle Pty 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 { Injectable } from '@angular/core';
|
||||||
|
import { Clipboard } from '@ionic-native/clipboard/ngx';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Emulates the Cordova Clipboard plugin in browser.
|
||||||
|
*/
|
||||||
|
@Injectable()
|
||||||
|
export class ClipboardMock extends Clipboard {
|
||||||
|
|
||||||
|
protected copyTextarea: HTMLTextAreaElement;
|
||||||
|
|
||||||
|
constructor() {
|
||||||
|
super();
|
||||||
|
|
||||||
|
// In browser the text must be selected in order to copy it. Create a hidden textarea to put the text in it.
|
||||||
|
this.copyTextarea = document.createElement('textarea');
|
||||||
|
this.copyTextarea.className = 'core-browser-copy-area';
|
||||||
|
this.copyTextarea.setAttribute('aria-hidden', 'true');
|
||||||
|
document.body.appendChild(this.copyTextarea);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Copy some text to the clipboard.
|
||||||
|
*
|
||||||
|
* @param text The text to copy.
|
||||||
|
* @return Promise resolved when copied.
|
||||||
|
*/
|
||||||
|
copy(text: string): Promise<void> {
|
||||||
|
return new Promise((resolve, reject): void => {
|
||||||
|
// Put the text in the hidden textarea and select it.
|
||||||
|
this.copyTextarea.innerHTML = text;
|
||||||
|
this.copyTextarea.select();
|
||||||
|
|
||||||
|
try {
|
||||||
|
if (document.execCommand('copy')) {
|
||||||
|
resolve();
|
||||||
|
} else {
|
||||||
|
reject();
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
reject();
|
||||||
|
}
|
||||||
|
|
||||||
|
this.copyTextarea.innerHTML = '';
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Get the text stored in the clipboard.
|
||||||
|
*
|
||||||
|
* @return Promise resolved with the text.
|
||||||
|
*/
|
||||||
|
paste(): Promise<string> {
|
||||||
|
return new Promise((resolve, reject): void => {
|
||||||
|
// Paste the text in the hidden textarea and get it.
|
||||||
|
this.copyTextarea.innerHTML = '';
|
||||||
|
this.copyTextarea.select();
|
||||||
|
|
||||||
|
try {
|
||||||
|
if (document.execCommand('paste')) {
|
||||||
|
resolve(this.copyTextarea.innerHTML);
|
||||||
|
} else {
|
||||||
|
reject();
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
reject();
|
||||||
|
}
|
||||||
|
|
||||||
|
this.copyTextarea.innerHTML = '';
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -0,0 +1,56 @@
|
||||||
|
// (C) Copyright 2015 Moodle Pty 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 { Injectable } from '@angular/core';
|
||||||
|
import { FileOpener } from '@ionic-native/file-opener/ngx';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Emulates the FileOpener plugin in browser.
|
||||||
|
*/
|
||||||
|
/* eslint-disable @typescript-eslint/no-unused-vars, @typescript-eslint/no-explicit-any */
|
||||||
|
@Injectable()
|
||||||
|
export class FileOpenerMock extends FileOpener {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if an app is already installed.
|
||||||
|
*
|
||||||
|
* @param packageId Package ID.
|
||||||
|
* @return Promise resolved when done.
|
||||||
|
*/
|
||||||
|
appIsInstalled(packageId: string): Promise<any> {
|
||||||
|
return Promise.reject('appIsInstalled not supported in browser.');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Open an file.
|
||||||
|
*
|
||||||
|
* @param filePath File path.
|
||||||
|
* @param fileMIMEType File MIME type.
|
||||||
|
* @return Promise resolved when done.
|
||||||
|
*/
|
||||||
|
async open(filePath: string, fileMIMEType: string): Promise<any> {
|
||||||
|
window.open(filePath, '_blank');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Uninstalls a package.
|
||||||
|
*
|
||||||
|
* @param packageId Package ID.
|
||||||
|
* @return Promise resolved when done.
|
||||||
|
*/
|
||||||
|
uninstall(packageId: string): Promise<any> {
|
||||||
|
return Promise.reject('uninstall not supported in browser.');
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -0,0 +1,379 @@
|
||||||
|
// (C) Copyright 2015 Moodle Pty 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 { CoreTextUtils } from '@/app/services/utils/text';
|
||||||
|
import { Injectable } from '@angular/core';
|
||||||
|
import { FileTransfer, FileTransferObject, FileUploadResult, FileTransferError } from '@ionic-native/file-transfer/ngx';
|
||||||
|
|
||||||
|
import { CoreFile } from '@services/file';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Mock the File Transfer Error.
|
||||||
|
*/
|
||||||
|
export class FileTransferErrorMock implements FileTransferError {
|
||||||
|
|
||||||
|
static readonly FILE_NOT_FOUND_ERR = 1;
|
||||||
|
static readonly INVALID_URL_ERR = 2;
|
||||||
|
static readonly CONNECTION_ERR = 3;
|
||||||
|
static readonly ABORT_ERR = 4;
|
||||||
|
static readonly 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 {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a new FileTransferObjectMock object.
|
||||||
|
*/
|
||||||
|
create(): FileTransferObjectMock {
|
||||||
|
return new FileTransferObjectMock();
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Emulates the FileTransferObject class in desktop apps and in browser.
|
||||||
|
*/
|
||||||
|
export class FileTransferObjectMock extends FileTransferObject {
|
||||||
|
|
||||||
|
progressListener?: (event: ProgressEvent) => void;
|
||||||
|
source?: string;
|
||||||
|
target?: string;
|
||||||
|
xhr?: XMLHttpRequest;
|
||||||
|
|
||||||
|
protected reject?: (reason?: unknown) => void;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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!, 0, '', ''));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Downloads a file from server.
|
||||||
|
*
|
||||||
|
* @param source URL of the server to download the file, as encoded by encodeURI().
|
||||||
|
* @param target Filesystem url representing the file on the device.
|
||||||
|
* @param trustAllHosts If set to true, it accepts all security certificates.
|
||||||
|
* @param options Optional parameters, currently only supports headers.
|
||||||
|
* @return Returns a Promise that resolves to a FileEntry object.
|
||||||
|
*/
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
|
download(source: string, target: string, trustAllHosts?: boolean, options?: { [s: string]: any }): Promise<unknown> {
|
||||||
|
return new Promise((resolve, reject): void => {
|
||||||
|
// Use XMLHttpRequest instead of HttpClient to support onprogress and abort.
|
||||||
|
const basicAuthHeader = this.getBasicAuthHeader(source);
|
||||||
|
const xhr = new XMLHttpRequest();
|
||||||
|
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
const headers = options?.headers || null;
|
||||||
|
|
||||||
|
// Prepare the request.
|
||||||
|
xhr.open('GET', source, true);
|
||||||
|
xhr.responseType = 'blob';
|
||||||
|
for (const name in headers) {
|
||||||
|
xhr.setRequestHeader(name, headers[name]);
|
||||||
|
}
|
||||||
|
|
||||||
|
xhr.onprogress = (ev: ProgressEvent): void => {
|
||||||
|
if (this.progressListener) {
|
||||||
|
this.progressListener(ev);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
xhr.onerror = (): void => {
|
||||||
|
reject(new FileTransferErrorMock(-1, source, target, xhr.status, xhr.statusText, ''));
|
||||||
|
};
|
||||||
|
|
||||||
|
xhr.onload = async (): Promise<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.
|
||||||
|
response = await this.parseResponse(response);
|
||||||
|
|
||||||
|
reject(new FileTransferErrorMock(-1, source, target, xhr.status, response || xhr.statusText, ''));
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!response) {
|
||||||
|
reject();
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const basePath = CoreFile.instance.getBasePathInstant();
|
||||||
|
target = target.replace(basePath, ''); // Remove basePath from the target.
|
||||||
|
target = target.replace(/%20/g, ' '); // Replace all %20 with spaces.
|
||||||
|
|
||||||
|
// eslint-disable-next-line promise/catch-or-return
|
||||||
|
CoreFile.instance.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 urlString The URL to get the credentials from.
|
||||||
|
* @return The header with the credentials, null if no credentials.
|
||||||
|
*/
|
||||||
|
protected getBasicAuthHeader(urlString: string): {name: string; value: string} | null {
|
||||||
|
let header: {name: string; value: string} | null = 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) {
|
||||||
|
header = {
|
||||||
|
name: 'Authorization',
|
||||||
|
value: 'Basic ' + window.btoa(credentials),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return header;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Given an instance of XMLHttpRequest, get the response headers as an object.
|
||||||
|
*
|
||||||
|
* @param xhr XMLHttpRequest instance.
|
||||||
|
* @return Object with the headers.
|
||||||
|
*/
|
||||||
|
protected getHeadersAsObject(xhr: XMLHttpRequest): Record<string, string> {
|
||||||
|
const headersString = xhr.getAllResponseHeaders();
|
||||||
|
const result = {};
|
||||||
|
|
||||||
|
if (headersString) {
|
||||||
|
const headers = headersString.split('\n');
|
||||||
|
for (const i in headers) {
|
||||||
|
const headerString = headers[i];
|
||||||
|
const 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 urlString The URL to get the credentials from.
|
||||||
|
* @return Retrieved credentials.
|
||||||
|
*/
|
||||||
|
protected getUrlCredentials(urlString: string): string | null {
|
||||||
|
const credentialsPattern = /^https?:\/\/(?:(?:(([^:@/]*)(?::([^@/]*))?)?@)?([^:/?#]*)(?::(\d*))?).*$/;
|
||||||
|
const credentials = credentialsPattern.exec(urlString);
|
||||||
|
|
||||||
|
return credentials && credentials[1];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Registers a listener that gets called whenever a new chunk of data is transferred.
|
||||||
|
*
|
||||||
|
* @param listener Listener that takes a progress event.
|
||||||
|
*/
|
||||||
|
onProgress(listener: (event: ProgressEvent) => void): void {
|
||||||
|
this.progressListener = listener;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Parse a response, converting it into text and the into an object if needed.
|
||||||
|
*
|
||||||
|
* @param response The response to parse.
|
||||||
|
* @return Promise resolved with the parsed response.
|
||||||
|
*/
|
||||||
|
protected async parseResponse(response: Blob | ArrayBuffer | string | null): Promise<unknown> {
|
||||||
|
if (!response) {
|
||||||
|
return '';
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
let responseText = '';
|
||||||
|
|
||||||
|
if (response instanceof Blob) {
|
||||||
|
responseText = await this.blobToText(response);
|
||||||
|
|
||||||
|
} else if (response instanceof ArrayBuffer) {
|
||||||
|
// Convert the ArrayBuffer into text.
|
||||||
|
responseText = String.fromCharCode.apply(null, new Uint8Array(response));
|
||||||
|
|
||||||
|
} else {
|
||||||
|
responseText = response;
|
||||||
|
}
|
||||||
|
|
||||||
|
return CoreTextUtils.instance.parseJSON(responseText, '');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Convert a Blob to text.
|
||||||
|
*
|
||||||
|
* @param blob Blob to convert.
|
||||||
|
* @return Promise resolved with blob contents.
|
||||||
|
*/
|
||||||
|
protected blobToText(blob: Blob): Promise<string> {
|
||||||
|
return new Promise<string>((resolve) => {
|
||||||
|
const reader = new FileReader();
|
||||||
|
reader.onloadend = (): void => {
|
||||||
|
resolve(<string> reader.result);
|
||||||
|
};
|
||||||
|
reader.readAsText(blob);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sends a file to a server.
|
||||||
|
*
|
||||||
|
* @param fileUrl Filesystem URL representing the file on the device or a data URI.
|
||||||
|
* @param url URL of the server to receive the file, as encoded by encodeURI().
|
||||||
|
* @param options Optional parameters.
|
||||||
|
* @return Promise that resolves to a FileUploadResult and rejects with FileTransferError.
|
||||||
|
*/
|
||||||
|
upload(fileUrl: string, url: string, options?: FileUploadOptions): Promise<FileUploadResult> {
|
||||||
|
return new Promise((resolve, reject): void => {
|
||||||
|
const basicAuthHeader = this.getBasicAuthHeader(url);
|
||||||
|
let fileKey: string | undefined;
|
||||||
|
let fileName: string | undefined;
|
||||||
|
let params: any; // eslint-disable-line @typescript-eslint/no-explicit-any
|
||||||
|
let headers: any; // eslint-disable-line @typescript-eslint/no-explicit-any
|
||||||
|
let httpMethod: string | undefined;
|
||||||
|
|
||||||
|
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';
|
||||||
|
}
|
||||||
|
|
||||||
|
params = options.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.
|
||||||
|
CoreFile.instance.getFile(fileUrl).then((fileEntry) =>
|
||||||
|
CoreFile.instance.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): void => {
|
||||||
|
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, ''));
|
||||||
|
};
|
||||||
|
|
||||||
|
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, fileName);
|
||||||
|
|
||||||
|
xhr.send(fd);
|
||||||
|
|
||||||
|
return;
|
||||||
|
}).catch(reject);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -0,0 +1,797 @@
|
||||||
|
// (C) Copyright 2015 Moodle Pty 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 { Injectable } from '@angular/core';
|
||||||
|
import { File, Entry, DirectoryEntry, FileEntry, IWriteOptions, RemoveResult } from '@ionic-native/file/ngx';
|
||||||
|
|
||||||
|
import { CoreTextUtils } from '@services/utils/text';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Implement the File Error because the ionic-native plugin doesn't implement it.
|
||||||
|
*/
|
||||||
|
class FileError {
|
||||||
|
|
||||||
|
static readonly NOT_FOUND_ERR = 1;
|
||||||
|
static readonly SECURITY_ERR = 2;
|
||||||
|
static readonly ABORT_ERR = 3;
|
||||||
|
static readonly NOT_READABLE_ERR = 4;
|
||||||
|
static readonly ENCODING_ERR = 5;
|
||||||
|
static readonly NO_MODIFICATION_ALLOWED_ERR = 6;
|
||||||
|
static readonly INVALID_STATE_ERR = 7;
|
||||||
|
static readonly SYNTAX_ERR = 8;
|
||||||
|
static readonly INVALID_MODIFICATION_ERR = 9;
|
||||||
|
static readonly QUOTA_EXCEEDED_ERR = 10;
|
||||||
|
static readonly TYPE_MISMATCH_ERR = 11;
|
||||||
|
static readonly PATH_EXISTS_ERR = 12;
|
||||||
|
|
||||||
|
message?: string;
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
public code: number,
|
||||||
|
) { }
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Emulates the Cordova File plugin in browser.
|
||||||
|
* Most of the code is extracted from the File class of Ionic Native.
|
||||||
|
*/
|
||||||
|
@Injectable()
|
||||||
|
export class FileMock extends File {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if a directory exists in a certain path, directory.
|
||||||
|
*
|
||||||
|
* @param path Base FileSystem.
|
||||||
|
* @param dir Name of directory to check
|
||||||
|
* @return Returns a Promise that resolves to true if the directory exists or rejects with an error.
|
||||||
|
*/
|
||||||
|
async checkDir(path: string, dir: string): Promise<boolean> {
|
||||||
|
const fullPath = CoreTextUtils.instance.concatenatePaths(path, dir);
|
||||||
|
|
||||||
|
await this.resolveDirectoryUrl(fullPath);
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if a file exists in a certain path, directory.
|
||||||
|
*
|
||||||
|
* @param path Base FileSystem.
|
||||||
|
* @param file Name of file to check.
|
||||||
|
* @return Returns a Promise that resolves with a boolean or rejects with an error.
|
||||||
|
*/
|
||||||
|
async checkFile(path: string, file: string): Promise<boolean> {
|
||||||
|
const entry = await this.resolveLocalFilesystemUrl(CoreTextUtils.instance.concatenatePaths(path, file));
|
||||||
|
|
||||||
|
if (entry.isFile) {
|
||||||
|
return true;
|
||||||
|
} else {
|
||||||
|
const error = new FileError(13);
|
||||||
|
error.message = 'input is not a file';
|
||||||
|
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Copy a file or directory.
|
||||||
|
*
|
||||||
|
* @param srce The Entry to copy.
|
||||||
|
* @param destDir The directory where to put the copy.
|
||||||
|
* @param newName New name of the file/dir.
|
||||||
|
* @return Returns a Promise that resolves to the new Entry object or rejects with an error.
|
||||||
|
*/
|
||||||
|
private copyMock(srce: Entry, destDir: DirectoryEntry, newName: string): Promise<Entry> {
|
||||||
|
return new Promise<Entry>((resolve, reject): void => {
|
||||||
|
newName = newName.replace(/%20/g, ' '); // Replace all %20 with spaces.
|
||||||
|
|
||||||
|
srce.copyTo(destDir, newName, (deste) => {
|
||||||
|
resolve(deste);
|
||||||
|
}, (err) => {
|
||||||
|
this.fillErrorMessageMock(err);
|
||||||
|
reject(err);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Copy a directory in various methods. If destination directory exists, will fail to copy.
|
||||||
|
*
|
||||||
|
* @param path Base FileSystem. Please refer to the iOS and Android filesystems above.
|
||||||
|
* @param dirName Name of directory to copy.
|
||||||
|
* @param newPath Base FileSystem of new location.
|
||||||
|
* @param newDirName New name of directory to copy to (leave blank to remain the same).
|
||||||
|
* @return Returns a Promise that resolves to the new Entry object or rejects with an error.
|
||||||
|
*/
|
||||||
|
copyDir(path: string, dirName: string, newPath: string, newDirName: string): Promise<Entry> {
|
||||||
|
return this.copyFileOrDir(path, dirName, newPath, newDirName);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Copy a file in various methods. If file exists, will fail to copy.
|
||||||
|
*
|
||||||
|
* @param path Base FileSystem. Please refer to the iOS and Android filesystems above
|
||||||
|
* @param fileName Name of file to copy
|
||||||
|
* @param newPath Base FileSystem of new location
|
||||||
|
* @param newFileName New name of file to copy to (leave blank to remain the same)
|
||||||
|
* @return Returns a Promise that resolves to an Entry or rejects with an error.
|
||||||
|
*/
|
||||||
|
copyFile(path: string, fileName: string, newPath: string, newFileName: string): Promise<Entry> {
|
||||||
|
return this.copyFileOrDir(path, fileName, newPath, newFileName || fileName);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Copy a file or dir to a given path.
|
||||||
|
*
|
||||||
|
* @param sourcePath Path of the file/dir to copy.
|
||||||
|
* @param sourceName Name of file/dir to copy
|
||||||
|
* @param destPath Path where to copy.
|
||||||
|
* @param destName New name of file/dir.
|
||||||
|
* @return Returns a Promise that resolves to the new Entry or rejects with an error.
|
||||||
|
*/
|
||||||
|
async copyFileOrDir(sourcePath: string, sourceName: string, destPath: string, destName: string): Promise<Entry> {
|
||||||
|
const destFixed = this.fixPathAndName(destPath, destName);
|
||||||
|
|
||||||
|
const source = await this.resolveLocalFilesystemUrl(CoreTextUtils.instance.concatenatePaths(sourcePath, sourceName));
|
||||||
|
|
||||||
|
const destParentDir = await this.resolveDirectoryUrl(destFixed.path);
|
||||||
|
|
||||||
|
return this.copyMock(source, destParentDir, destFixed.name);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a new directory in the specific path.
|
||||||
|
* The replace boolean value determines whether to replace an existing directory with the same name.
|
||||||
|
* If an existing directory exists and the replace value is false, the promise will fail and return an error.
|
||||||
|
*
|
||||||
|
* @param path Base FileSystem.
|
||||||
|
* @param dirName Name of directory to create
|
||||||
|
* @param replace If true, replaces file with same name. If false returns error
|
||||||
|
* @return Returns a Promise that resolves with a DirectoryEntry or rejects with an error.
|
||||||
|
*/
|
||||||
|
async createDir(path: string, dirName: string, replace: boolean): Promise<DirectoryEntry> {
|
||||||
|
const options: Flags = {
|
||||||
|
create: true,
|
||||||
|
};
|
||||||
|
|
||||||
|
if (!replace) {
|
||||||
|
options.exclusive = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
const parentDir = await this.resolveDirectoryUrl(path);
|
||||||
|
|
||||||
|
return this.getDirectory(parentDir, dirName, options);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a new file in the specific path.
|
||||||
|
* The replace boolean value determines whether to replace an existing file with the same name.
|
||||||
|
* If an existing file exists and the replace value is false, the promise will fail and return an error.
|
||||||
|
*
|
||||||
|
* @param path Base FileSystem.
|
||||||
|
* @param fileName Name of file to create.
|
||||||
|
* @param replace If true, replaces file with same name. If false returns error.
|
||||||
|
* @return Returns a Promise that resolves to a FileEntry or rejects with an error.
|
||||||
|
*/
|
||||||
|
async createFile(path: string, fileName: string, replace: boolean): Promise<FileEntry> {
|
||||||
|
const options: Flags = {
|
||||||
|
create: true,
|
||||||
|
};
|
||||||
|
|
||||||
|
if (!replace) {
|
||||||
|
options.exclusive = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
const parentDir = await this.resolveDirectoryUrl(path);
|
||||||
|
|
||||||
|
return this.getFile(parentDir, fileName, options);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a file writer for a certain file.
|
||||||
|
*
|
||||||
|
* @param fe File entry object.
|
||||||
|
* @return Promise resolved with the FileWriter.
|
||||||
|
*/
|
||||||
|
private createWriterMock(fe: FileEntry): Promise<FileWriter> {
|
||||||
|
return new Promise<FileWriter>((resolve, reject): void => {
|
||||||
|
fe.createWriter((writer) => {
|
||||||
|
resolve(writer);
|
||||||
|
}, (err) => {
|
||||||
|
this.fillErrorMessageMock(err);
|
||||||
|
reject(err);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Fill the message for an error.
|
||||||
|
*
|
||||||
|
* @param error Error.
|
||||||
|
*/
|
||||||
|
private fillErrorMessageMock(error: FileError): void {
|
||||||
|
try {
|
||||||
|
error.message = this.cordovaFileError[error.code];
|
||||||
|
} catch (e) {
|
||||||
|
// Ignore errors.
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get a directory.
|
||||||
|
*
|
||||||
|
* @param directoryEntry Directory entry, obtained by resolveDirectoryUrl method
|
||||||
|
* @param directoryName Directory name
|
||||||
|
* @param flags Options
|
||||||
|
*/
|
||||||
|
getDirectory(directoryEntry: DirectoryEntry, directoryName: string, flags: Flags): Promise<DirectoryEntry> {
|
||||||
|
return new Promise<DirectoryEntry>((resolve, reject): void => {
|
||||||
|
try {
|
||||||
|
directoryName = directoryName.replace(/%20/g, ' '); // Replace all %20 with spaces.
|
||||||
|
|
||||||
|
directoryEntry.getDirectory(directoryName, flags, (de) => {
|
||||||
|
resolve(de);
|
||||||
|
}, (err) => {
|
||||||
|
this.fillErrorMessageMock(err);
|
||||||
|
reject(err);
|
||||||
|
});
|
||||||
|
} catch (xc) {
|
||||||
|
this.fillErrorMessageMock(xc);
|
||||||
|
reject(xc);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get a file.
|
||||||
|
*
|
||||||
|
* @param directoryEntry Directory entry, obtained by resolveDirectoryUrl method
|
||||||
|
* @param fileName File name
|
||||||
|
* @param flags Options
|
||||||
|
*/
|
||||||
|
getFile(directoryEntry: DirectoryEntry, fileName: string, flags: Flags): Promise<FileEntry> {
|
||||||
|
return new Promise<FileEntry>((resolve, reject): void => {
|
||||||
|
try {
|
||||||
|
fileName = fileName.replace(/%20/g, ' '); // Replace all %20 with spaces.
|
||||||
|
|
||||||
|
directoryEntry.getFile(fileName, flags, resolve, (err) => {
|
||||||
|
this.fillErrorMessageMock(err);
|
||||||
|
reject(err);
|
||||||
|
});
|
||||||
|
} catch (xc) {
|
||||||
|
this.fillErrorMessageMock(xc);
|
||||||
|
reject(xc);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get free disk space.
|
||||||
|
*
|
||||||
|
* @return Promise resolved with the free space.
|
||||||
|
*/
|
||||||
|
async getFreeDiskSpace(): Promise<number> {
|
||||||
|
// Request a file system instance with a minimum size until we get an error.
|
||||||
|
if (window.requestFileSystem) {
|
||||||
|
let iterations = 0;
|
||||||
|
let maxIterations = 50;
|
||||||
|
const calculateByRequest = (size: number, ratio: number): Promise<number> =>
|
||||||
|
new Promise((resolve): void => {
|
||||||
|
window.requestFileSystem(LocalFileSystem.PERSISTENT, size, () => {
|
||||||
|
iterations++;
|
||||||
|
if (iterations > maxIterations) {
|
||||||
|
resolve(size);
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// eslint-disable-next-line promise/catch-or-return
|
||||||
|
calculateByRequest(size * ratio, ratio).then(resolve);
|
||||||
|
}, () => {
|
||||||
|
resolve(size / ratio);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// General calculation, base 1MB and increasing factor 1.3.
|
||||||
|
let size = await calculateByRequest(1048576, 1.3);
|
||||||
|
|
||||||
|
// More accurate. Factor is 1.1.
|
||||||
|
iterations = 0;
|
||||||
|
maxIterations = 10;
|
||||||
|
|
||||||
|
size = await calculateByRequest(size, 1.1);
|
||||||
|
|
||||||
|
return size / 1024; // Return size in KB.
|
||||||
|
|
||||||
|
} else {
|
||||||
|
throw new Error('File system not available.');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* List files and directory from a given path.
|
||||||
|
*
|
||||||
|
* @param path Base FileSystem. Please refer to the iOS and Android filesystems above
|
||||||
|
* @param dirName Name of directory
|
||||||
|
* @return Returns a Promise that resolves to an array of Entry objects or rejects with an error.
|
||||||
|
*/
|
||||||
|
async listDir(path: string, dirName: string): Promise<Entry[]> {
|
||||||
|
const parentDir = await this.resolveDirectoryUrl(path);
|
||||||
|
|
||||||
|
const dirEntry = await this.getDirectory(parentDir, dirName, { create: false, exclusive: false });
|
||||||
|
|
||||||
|
return this.readEntriesMock(dirEntry.createReader());
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Loads an initialize the API for browser.
|
||||||
|
*
|
||||||
|
* @return Promise resolved when loaded.
|
||||||
|
*/
|
||||||
|
load(): Promise<string> {
|
||||||
|
return new Promise((resolve, reject): void => {
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
|
const win = <any> window; // Convert to <any> to be able to use non-standard properties.
|
||||||
|
|
||||||
|
if (typeof win.requestFileSystem == 'undefined') {
|
||||||
|
win.requestFileSystem = win.webkitRequestFileSystem;
|
||||||
|
}
|
||||||
|
if (typeof win.resolveLocalFileSystemURL == 'undefined') {
|
||||||
|
win.resolveLocalFileSystemURL = win.webkitResolveLocalFileSystemURL;
|
||||||
|
}
|
||||||
|
win.LocalFileSystem = {
|
||||||
|
PERSISTENT: 1, // eslint-disable-line @typescript-eslint/naming-convention
|
||||||
|
};
|
||||||
|
|
||||||
|
// Request a quota to use. Request 500MB.
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
|
(<any> navigator).webkitPersistentStorage.requestQuota(500 * 1024 * 1024, (granted) => {
|
||||||
|
window.requestFileSystem(LocalFileSystem.PERSISTENT, granted, (entry) => {
|
||||||
|
resolve(entry.root.toURL());
|
||||||
|
}, reject);
|
||||||
|
}, reject);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Move a file or directory.
|
||||||
|
*
|
||||||
|
* @param srce The Entry to copy.
|
||||||
|
* @param destDir The directory where to move the file/dir.
|
||||||
|
* @param newName New name of the file/dir.
|
||||||
|
* @return Returns a Promise that resolves to the new Entry object or rejects with an error.
|
||||||
|
*/
|
||||||
|
private moveMock(srce: Entry, destDir: DirectoryEntry, newName: string): Promise<Entry> {
|
||||||
|
return new Promise<Entry>((resolve, reject): void => {
|
||||||
|
newName = newName.replace(/%20/g, ' '); // Replace all %20 with spaces.
|
||||||
|
|
||||||
|
srce.moveTo(destDir, newName, (deste) => {
|
||||||
|
resolve(deste);
|
||||||
|
}, (err) => {
|
||||||
|
this.fillErrorMessageMock(err);
|
||||||
|
reject(err);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Move a directory to a given path.
|
||||||
|
*
|
||||||
|
* @param path The source path to the directory.
|
||||||
|
* @param dirName The source directory name.
|
||||||
|
* @param newPath The destionation path to the directory.
|
||||||
|
* @param newDirName The destination directory name.
|
||||||
|
* @return Returns a Promise that resolves to the new DirectoryEntry object or rejects with
|
||||||
|
* an error.
|
||||||
|
*/
|
||||||
|
moveDir(path: string, dirName: string, newPath: string, newDirName: string): Promise<DirectoryEntry | Entry> {
|
||||||
|
return this.moveFileOrDir(path, dirName, newPath, newDirName);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Move a file to a given path.
|
||||||
|
*
|
||||||
|
* @param path Base FileSystem. Please refer to the iOS and Android filesystems above
|
||||||
|
* @param fileName Name of file to move
|
||||||
|
* @param newPath Base FileSystem of new location
|
||||||
|
* @param newFileName New name of file to move to (leave blank to remain the same)
|
||||||
|
* @return Returns a Promise that resolves to the new Entry or rejects with an error.
|
||||||
|
*/
|
||||||
|
moveFile(path: string, fileName: string, newPath: string, newFileName: string): Promise<Entry> {
|
||||||
|
return this.moveFileOrDir(path, fileName, newPath, newFileName || fileName);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Move a file or dir to a given path.
|
||||||
|
*
|
||||||
|
* @param sourcePath Path of the file/dir to copy.
|
||||||
|
* @param sourceName Name of file/dir to copy
|
||||||
|
* @param destPath Path where to copy.
|
||||||
|
* @param destName New name of file/dir.
|
||||||
|
* @return Returns a Promise that resolves to the new Entry or rejects with an error.
|
||||||
|
*/
|
||||||
|
async moveFileOrDir(sourcePath: string, sourceName: string, destPath: string, destName: string): Promise<Entry> {
|
||||||
|
const destFixed = this.fixPathAndName(destPath, destName);
|
||||||
|
|
||||||
|
const source = await this.resolveLocalFilesystemUrl(CoreTextUtils.instance.concatenatePaths(sourcePath, sourceName));
|
||||||
|
|
||||||
|
const destParentDir = await this.resolveDirectoryUrl(destFixed.path);
|
||||||
|
|
||||||
|
return this.moveMock(source, destParentDir, destFixed.name);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Fix a path and name, making sure the name doesn't contain any folder. If it does, the folder will be moved to the path.
|
||||||
|
*
|
||||||
|
* @param path Path to fix.
|
||||||
|
* @param name Name to fix.
|
||||||
|
* @return Fixed values.
|
||||||
|
*/
|
||||||
|
protected fixPathAndName(path: string, name: string): {path: string; name: string} {
|
||||||
|
|
||||||
|
const fullPath = CoreTextUtils.instance.concatenatePaths(path, name);
|
||||||
|
|
||||||
|
return {
|
||||||
|
path: fullPath.substring(0, fullPath.lastIndexOf('/')),
|
||||||
|
name: fullPath.substr(fullPath.lastIndexOf('/') + 1),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Read file and return data as an ArrayBuffer.
|
||||||
|
*
|
||||||
|
* @param path Base FileSystem.
|
||||||
|
* @param file Name of file, relative to path.
|
||||||
|
* @return Returns a Promise that resolves with the contents of the file as ArrayBuffer or rejects
|
||||||
|
* with an error.
|
||||||
|
*/
|
||||||
|
readAsArrayBuffer(path: string, file: string): Promise<ArrayBuffer> {
|
||||||
|
return this.readFileMock<ArrayBuffer>(path, file, 'ArrayBuffer');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Read file and return data as a binary data.
|
||||||
|
*
|
||||||
|
* @param path Base FileSystem.
|
||||||
|
* @param file Name of file, relative to path.
|
||||||
|
* @return Returns a Promise that resolves with the contents of the file as string rejects with an error.
|
||||||
|
*/
|
||||||
|
readAsBinaryString(path: string, file: string): Promise<string> {
|
||||||
|
return this.readFileMock<string>(path, file, 'BinaryString');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Read file and return data as a base64 encoded data url.
|
||||||
|
* A data url is of the form:
|
||||||
|
* data: [<mediatype>][;base64],<data>
|
||||||
|
*
|
||||||
|
* @param path Base FileSystem.
|
||||||
|
* @param file Name of file, relative to path.
|
||||||
|
* @return Returns a Promise that resolves with the contents of the file as data URL or rejects
|
||||||
|
* with an error.
|
||||||
|
*/
|
||||||
|
readAsDataURL(path: string, file: string): Promise<string> {
|
||||||
|
return this.readFileMock<string>(path, file, 'DataURL');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Read the contents of a file as text.
|
||||||
|
*
|
||||||
|
* @param path Base FileSystem.
|
||||||
|
* @param file Name of file, relative to path.
|
||||||
|
* @return Returns a Promise that resolves with the contents of the file as string or rejects with an error.
|
||||||
|
*/
|
||||||
|
readAsText(path: string, file: string): Promise<string> {
|
||||||
|
return this.readFileMock<string>(path, file, 'Text');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Read all the files and directories inside a directory.
|
||||||
|
*
|
||||||
|
* @param directoryReader The directory reader.
|
||||||
|
* @return Promise resolved with the list of files/dirs.
|
||||||
|
*/
|
||||||
|
private readEntriesMock(directoryReader: DirectoryReader): Promise<Entry[]> {
|
||||||
|
return new Promise<Entry[]>((resolve, reject): void => {
|
||||||
|
directoryReader.readEntries((entries: Entry[]) => {
|
||||||
|
resolve(entries);
|
||||||
|
}, (error: FileError) => {
|
||||||
|
this.fillErrorMessageMock(error);
|
||||||
|
reject(error);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Read the contents of a file.
|
||||||
|
*
|
||||||
|
* @param path Base FileSystem.
|
||||||
|
* @param file Name of file, relative to path.
|
||||||
|
* @param readAs Format to read as.
|
||||||
|
* @return Returns a Promise that resolves with the contents of the file or rejects with an error.
|
||||||
|
*/
|
||||||
|
private async readFileMock<T>(
|
||||||
|
path: string,
|
||||||
|
file: string,
|
||||||
|
readAs: 'ArrayBuffer' | 'BinaryString' | 'DataURL' | 'Text',
|
||||||
|
): Promise<T> {
|
||||||
|
const directoryEntry = await this.resolveDirectoryUrl(path);
|
||||||
|
|
||||||
|
const fileEntry = await this.getFile(directoryEntry, file, { create: false });
|
||||||
|
|
||||||
|
const reader = new FileReader();
|
||||||
|
|
||||||
|
return new Promise<T>((resolve, reject): void => {
|
||||||
|
reader.onloadend = (): void => {
|
||||||
|
if (reader.result !== undefined || reader.result !== null) {
|
||||||
|
resolve(<T> <unknown> reader.result);
|
||||||
|
} else if (reader.error !== undefined || reader.error !== null) {
|
||||||
|
reject(reader.error);
|
||||||
|
} else {
|
||||||
|
reject({ code: null, message: 'READER_ONLOADEND_ERR' });
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
fileEntry.file((file) => {
|
||||||
|
reader[`readAs${readAs}`].call(reader, file);
|
||||||
|
}, (error) => {
|
||||||
|
reject(error);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Delete a file.
|
||||||
|
*
|
||||||
|
* @param entry The file to remove.
|
||||||
|
* @return Promise resolved when done.
|
||||||
|
*/
|
||||||
|
private removeMock(entry: Entry): Promise<RemoveResult> {
|
||||||
|
return new Promise<RemoveResult>((resolve, reject): void => {
|
||||||
|
entry.remove(() => {
|
||||||
|
resolve({ success: true, fileRemoved: entry });
|
||||||
|
}, (err) => {
|
||||||
|
this.fillErrorMessageMock(err);
|
||||||
|
reject(err);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Remove a directory at a given path.
|
||||||
|
*
|
||||||
|
* @param path The path to the directory.
|
||||||
|
* @param dirName The directory name.
|
||||||
|
* @return Returns a Promise that resolves to a RemoveResult or rejects with an error.
|
||||||
|
*/
|
||||||
|
async removeDir(path: string, dirName: string): Promise<RemoveResult> {
|
||||||
|
const parentDir = await this.resolveDirectoryUrl(path);
|
||||||
|
|
||||||
|
const dirEntry = await this.getDirectory(parentDir, dirName, { create: false });
|
||||||
|
|
||||||
|
return this.removeMock(dirEntry);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Removes a file from a desired location.
|
||||||
|
*
|
||||||
|
* @param path Base FileSystem.
|
||||||
|
* @param fileName Name of file to remove.
|
||||||
|
* @return Returns a Promise that resolves to a RemoveResult or rejects with an error.
|
||||||
|
*/
|
||||||
|
async removeFile(path: string, fileName: string): Promise<RemoveResult> {
|
||||||
|
const parentDir = await this.resolveDirectoryUrl(path);
|
||||||
|
|
||||||
|
const fileEntry = await this.getFile(parentDir, fileName, { create: false });
|
||||||
|
|
||||||
|
return this.removeMock(fileEntry);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Removes all files and the directory from a desired location.
|
||||||
|
*
|
||||||
|
* @param path Base FileSystem. Please refer to the iOS and Android filesystems above
|
||||||
|
* @param dirName Name of directory
|
||||||
|
* @return Returns a Promise that resolves with a RemoveResult or rejects with an error.
|
||||||
|
*/
|
||||||
|
async removeRecursively(path: string, dirName: string): Promise<RemoveResult> {
|
||||||
|
const parentDir = await this.resolveDirectoryUrl(path);
|
||||||
|
|
||||||
|
const dirEntry = await this.getDirectory(parentDir, dirName, { create: false });
|
||||||
|
|
||||||
|
return this.rimrafMock(dirEntry);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Resolves a local directory url.
|
||||||
|
*
|
||||||
|
* @param directoryUrl directory system url
|
||||||
|
*/
|
||||||
|
async resolveDirectoryUrl(directoryUrl: string): Promise<DirectoryEntry> {
|
||||||
|
const dirEntry = await this.resolveLocalFilesystemUrl(directoryUrl);
|
||||||
|
|
||||||
|
if (dirEntry.isDirectory) {
|
||||||
|
return <DirectoryEntry> dirEntry;
|
||||||
|
} else {
|
||||||
|
const error = new FileError(13);
|
||||||
|
error.message = 'input is not a directory';
|
||||||
|
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Resolves a local file system URL.
|
||||||
|
*
|
||||||
|
* @param fileUrl file system url
|
||||||
|
*/
|
||||||
|
resolveLocalFilesystemUrl(fileUrl: string): Promise<Entry> {
|
||||||
|
return new Promise<Entry>((resolve, reject): void => {
|
||||||
|
try {
|
||||||
|
window.resolveLocalFileSystemURL(fileUrl, (entry: Entry) => {
|
||||||
|
resolve(entry);
|
||||||
|
}, (error: FileError) => {
|
||||||
|
this.fillErrorMessageMock(error);
|
||||||
|
reject(error);
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
this.fillErrorMessageMock(error);
|
||||||
|
reject(error);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Remove a directory and all its contents.
|
||||||
|
*
|
||||||
|
* @param de Directory to remove.
|
||||||
|
* @return Promise resolved when done.
|
||||||
|
*/
|
||||||
|
private rimrafMock(de: DirectoryEntry): Promise<RemoveResult> {
|
||||||
|
return new Promise<RemoveResult>((resolve, reject): void => {
|
||||||
|
de.removeRecursively(() => {
|
||||||
|
resolve({ success: true, fileRemoved: de });
|
||||||
|
}, (err) => {
|
||||||
|
this.fillErrorMessageMock(err);
|
||||||
|
reject(err);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Write some data in a file.
|
||||||
|
*
|
||||||
|
* @param writer File writer.
|
||||||
|
* @param data The data to write.
|
||||||
|
* @return Promise resolved when done.
|
||||||
|
*/
|
||||||
|
protected writeMock(writer: FileWriter, data: string | Blob | ArrayBuffer): Promise<void> {
|
||||||
|
if (data instanceof Blob) {
|
||||||
|
return this.writeFileInChunksMock(writer, data);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (data instanceof ArrayBuffer) {
|
||||||
|
// Convert to string.
|
||||||
|
data = String.fromCharCode.apply(null, new Uint8Array(data));
|
||||||
|
}
|
||||||
|
|
||||||
|
return new Promise<void>((resolve, reject) => {
|
||||||
|
writer.onwriteend = (): void => {
|
||||||
|
if (writer.error) {
|
||||||
|
reject(writer.error);
|
||||||
|
} else {
|
||||||
|
resolve();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
writer.write(<string> data);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Write to an existing file.
|
||||||
|
*
|
||||||
|
* @param path Base FileSystem.
|
||||||
|
* @param fileName path relative to base path.
|
||||||
|
* @param text content or blob to write.
|
||||||
|
* @return Returns a Promise that resolves or rejects with an error.
|
||||||
|
*/
|
||||||
|
async writeExistingFile(path: string, fileName: string, text: string | Blob): Promise<void> {
|
||||||
|
await this.writeFile(path, fileName, text, { replace: true });
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Write a new file to the desired location.
|
||||||
|
*
|
||||||
|
* @param path Base FileSystem. Please refer to the iOS and Android filesystems above
|
||||||
|
* @param fileName path relative to base path
|
||||||
|
* @param text content or blob to write
|
||||||
|
* @param options replace file if set to true. See WriteOptions for more information.
|
||||||
|
* @return Returns a Promise that resolves to updated file entry or rejects with an error.
|
||||||
|
*/
|
||||||
|
async writeFile(
|
||||||
|
path: string,
|
||||||
|
fileName: string,
|
||||||
|
text: string | Blob | ArrayBuffer,
|
||||||
|
options: IWriteOptions = {},
|
||||||
|
): Promise<FileEntry> {
|
||||||
|
const getFileOpts: Flags = {
|
||||||
|
create: !options.append,
|
||||||
|
exclusive: !options.replace,
|
||||||
|
};
|
||||||
|
|
||||||
|
const parentDir = await this.resolveDirectoryUrl(path);
|
||||||
|
|
||||||
|
const fileEntry = await this.getFile(parentDir, fileName, getFileOpts);
|
||||||
|
|
||||||
|
return this.writeFileEntryMock(fileEntry, text, options);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Write content to FileEntry.
|
||||||
|
*
|
||||||
|
* @param fe File entry object.
|
||||||
|
* @param text Content or blob to write.
|
||||||
|
* @param options replace file if set to true. See WriteOptions for more information.
|
||||||
|
* @return Returns a Promise that resolves to updated file entry or rejects with an error.
|
||||||
|
*/
|
||||||
|
private writeFileEntryMock(
|
||||||
|
fileEntry: FileEntry,
|
||||||
|
text: string | Blob | ArrayBuffer,
|
||||||
|
options: IWriteOptions,
|
||||||
|
): Promise<FileEntry> {
|
||||||
|
return this.createWriterMock(fileEntry).then((writer) => {
|
||||||
|
if (options.append) {
|
||||||
|
writer.seek(writer.length);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (options.truncate) {
|
||||||
|
writer.truncate(options.truncate);
|
||||||
|
}
|
||||||
|
|
||||||
|
return this.writeMock(writer, text);
|
||||||
|
}).then(() => fileEntry);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Write a file in chunks.
|
||||||
|
*
|
||||||
|
* @param writer File writer.
|
||||||
|
* @param data Data to write.
|
||||||
|
* @return Promise resolved when done.
|
||||||
|
*/
|
||||||
|
private writeFileInChunksMock(writer: FileWriter, data: Blob): Promise<void> {
|
||||||
|
let writtenSize = 0;
|
||||||
|
const BLOCK_SIZE = 1024 * 1024;
|
||||||
|
const writeNextChunk = () => {
|
||||||
|
const size = Math.min(BLOCK_SIZE, data.size - writtenSize);
|
||||||
|
const chunk = data.slice(writtenSize, writtenSize + size);
|
||||||
|
|
||||||
|
writtenSize += size;
|
||||||
|
writer.write(chunk);
|
||||||
|
};
|
||||||
|
|
||||||
|
return new Promise<void>((resolve, reject): void => {
|
||||||
|
writer.onerror = reject;
|
||||||
|
writer.onwriteend = (): void => {
|
||||||
|
if (writtenSize < data.size) {
|
||||||
|
writeNextChunk();
|
||||||
|
} else {
|
||||||
|
resolve();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
writeNextChunk();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -0,0 +1,61 @@
|
||||||
|
// (C) Copyright 2015 Moodle Pty 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 { Injectable } from '@angular/core';
|
||||||
|
import { Geolocation, GeolocationOptions, Geoposition } from '@ionic-native/geolocation/ngx';
|
||||||
|
import { Observable, Subscriber, TeardownLogic } from 'rxjs';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Emulates the Cordova Geolocation plugin in desktop apps and in browser.
|
||||||
|
*/
|
||||||
|
@Injectable()
|
||||||
|
export class GeolocationMock extends Geolocation {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the device's current position.
|
||||||
|
*
|
||||||
|
* @param options The geolocation options.
|
||||||
|
* @returns Returns a Promise that resolves with the position of the device, or rejects with an error.
|
||||||
|
*/
|
||||||
|
getCurrentPosition(options?: GeolocationOptions): Promise<Geoposition> {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
navigator.geolocation.getCurrentPosition((position) => {
|
||||||
|
// Convert to unknown first because some fields are incompatible due to null values.
|
||||||
|
resolve(<Geoposition> <unknown> position);
|
||||||
|
}, reject, options);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Watch the current device's position. Clear the watch by unsubscribing from
|
||||||
|
* Observable changes.
|
||||||
|
*
|
||||||
|
* @param options The geolocation options.
|
||||||
|
* @returns Returns an Observable that notifies with the position of the device, or errors.
|
||||||
|
*/
|
||||||
|
watchPosition(options?: GeolocationOptions): Observable<Geoposition> {
|
||||||
|
return new Observable<Geoposition>((subscriber: Subscriber<Geoposition>): TeardownLogic => {
|
||||||
|
const watchId = navigator.geolocation.watchPosition(
|
||||||
|
subscriber.next.bind(subscriber),
|
||||||
|
subscriber.error.bind(subscriber),
|
||||||
|
options,
|
||||||
|
);
|
||||||
|
|
||||||
|
return (): void => {
|
||||||
|
navigator.geolocation.clearWatch(watchId);
|
||||||
|
};
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -0,0 +1,63 @@
|
||||||
|
// (C) Copyright 2015 Moodle Pty 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 { Injectable } from '@angular/core';
|
||||||
|
import { File } from '@ionic-native/file/ngx';
|
||||||
|
|
||||||
|
import { CoreFile } from '@services/file';
|
||||||
|
import { CoreInitDelegate, CoreInitHandler } from '@services/init';
|
||||||
|
import { CoreUtils } from '@services/utils/utils';
|
||||||
|
import { CoreLogger } from '@singletons/logger';
|
||||||
|
import { FileMock } from './file';
|
||||||
|
import { FileTransferErrorMock } from './file-transfer';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Helper service for the emulator feature. It also acts as an init handler.
|
||||||
|
*/
|
||||||
|
@Injectable()
|
||||||
|
export class CoreEmulatorHelperProvider implements CoreInitHandler {
|
||||||
|
|
||||||
|
name = 'CoreEmulator';
|
||||||
|
priority = CoreInitDelegate.MAX_RECOMMENDED_PRIORITY + 500;
|
||||||
|
blocking = true;
|
||||||
|
|
||||||
|
protected logger: CoreLogger;
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
protected file: File,
|
||||||
|
) {
|
||||||
|
this.logger = CoreLogger.getInstance('CoreEmulatorHelper');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Load the Mocks that need it.
|
||||||
|
*
|
||||||
|
* @return Promise resolved when loaded.
|
||||||
|
*/
|
||||||
|
load(): Promise<void> {
|
||||||
|
const promises: Promise<unknown>[] = [];
|
||||||
|
|
||||||
|
window.FileTransferError = FileTransferErrorMock;
|
||||||
|
|
||||||
|
promises.push((<FileMock> this.file).load().then((basePath: string) => {
|
||||||
|
CoreFile.instance.setHTMLBasePath(basePath);
|
||||||
|
|
||||||
|
return;
|
||||||
|
}));
|
||||||
|
|
||||||
|
|
||||||
|
return CoreUtils.instance.allPromises(promises);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -0,0 +1,41 @@
|
||||||
|
// (C) Copyright 2015 Moodle Pty 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 { Injectable } from '@angular/core';
|
||||||
|
import { InAppBrowser, InAppBrowserObject } from '@ionic-native/in-app-browser/ngx';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Emulates the Cordova InAppBrowser plugin in desktop apps.
|
||||||
|
*/
|
||||||
|
@Injectable()
|
||||||
|
export class InAppBrowserMock extends InAppBrowser {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Opens a URL in a new InAppBrowser instance, the current browser instance, or the system browser.
|
||||||
|
*
|
||||||
|
* @param url The URL to load.
|
||||||
|
* @param target The target in which to load the URL, an optional parameter that defaults to _self.
|
||||||
|
* @param options Options for the InAppBrowser.
|
||||||
|
* @return The new instance.
|
||||||
|
*/
|
||||||
|
create(url: string, target?: string, options: string = 'location=yes'): InAppBrowserObject {
|
||||||
|
if (options && typeof options !== 'string') {
|
||||||
|
// Convert to string.
|
||||||
|
options = Object.keys(options).map((key) => key + '=' + options[key]).join(',');
|
||||||
|
}
|
||||||
|
|
||||||
|
return super.create(url, target, options);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -0,0 +1,82 @@
|
||||||
|
// (C) Copyright 2015 Moodle Pty 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 { Injectable } from '@angular/core';
|
||||||
|
import { Network } from '@ionic-native/network/ngx';
|
||||||
|
import { Observable, Subject, merge } from 'rxjs';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Emulates the Cordova Network plugin in browser.
|
||||||
|
*/
|
||||||
|
@Injectable()
|
||||||
|
export class NetworkMock extends Network {
|
||||||
|
|
||||||
|
type!: string;
|
||||||
|
|
||||||
|
constructor() {
|
||||||
|
super();
|
||||||
|
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
|
(<any> window).Connection = {
|
||||||
|
UNKNOWN: 'unknown', // eslint-disable-line @typescript-eslint/naming-convention
|
||||||
|
ETHERNET: 'ethernet', // eslint-disable-line @typescript-eslint/naming-convention
|
||||||
|
WIFI: 'wifi', // eslint-disable-line @typescript-eslint/naming-convention
|
||||||
|
CELL_2G: '2g', // eslint-disable-line @typescript-eslint/naming-convention
|
||||||
|
CELL_3G: '3g', // eslint-disable-line @typescript-eslint/naming-convention
|
||||||
|
CELL_4G: '4g', // eslint-disable-line @typescript-eslint/naming-convention
|
||||||
|
CELL: 'cellular', // eslint-disable-line @typescript-eslint/naming-convention
|
||||||
|
NONE: 'none', // eslint-disable-line @typescript-eslint/naming-convention
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns an observable to watch connection changes.
|
||||||
|
*
|
||||||
|
* @return Observable.
|
||||||
|
*/
|
||||||
|
onchange(): Observable<unknown> {
|
||||||
|
return merge(this.onConnect(), this.onDisconnect());
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns an observable to notify when the app is connected.
|
||||||
|
*
|
||||||
|
* @return Observable.
|
||||||
|
*/
|
||||||
|
onConnect(): Observable<unknown> {
|
||||||
|
const observable = new Subject<unknown>();
|
||||||
|
|
||||||
|
window.addEventListener('online', (ev) => {
|
||||||
|
observable.next(ev);
|
||||||
|
}, false);
|
||||||
|
|
||||||
|
return observable;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns an observable to notify when the app is disconnected.
|
||||||
|
*
|
||||||
|
* @return Observable.
|
||||||
|
*/
|
||||||
|
onDisconnect(): Observable<unknown> {
|
||||||
|
const observable = new Subject<unknown>();
|
||||||
|
|
||||||
|
window.addEventListener('offline', (ev) => {
|
||||||
|
observable.next(ev);
|
||||||
|
}, false);
|
||||||
|
|
||||||
|
return observable;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -0,0 +1,129 @@
|
||||||
|
// (C) Copyright 2015 Moodle Pty 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 { Injectable } from '@angular/core';
|
||||||
|
import { File } from '@ionic-native/file/ngx';
|
||||||
|
import { Zip } from '@ionic-native/zip/ngx';
|
||||||
|
import * as JSZip from 'jszip';
|
||||||
|
|
||||||
|
import { CoreTextUtils } from '@services/utils/text';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Emulates the Cordova Zip plugin in browser.
|
||||||
|
*/
|
||||||
|
@Injectable()
|
||||||
|
export class ZipMock extends Zip {
|
||||||
|
|
||||||
|
constructor(private file: File) {
|
||||||
|
super();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a directory. It creates all the foldes in dirPath 1 by 1 to prevent errors.
|
||||||
|
*
|
||||||
|
* @param destination Destination parent folder.
|
||||||
|
* @param dirPath Relative path to the folder.
|
||||||
|
* @return Promise resolved when done.
|
||||||
|
*/
|
||||||
|
protected async createDir(destination: string, dirPath: string): Promise<void> {
|
||||||
|
// Create all the folders 1 by 1 in order, otherwise it fails.
|
||||||
|
const folders = dirPath.split('/');
|
||||||
|
|
||||||
|
for (let i = 0; i < folders.length; i++) {
|
||||||
|
const folder = folders[i];
|
||||||
|
|
||||||
|
await this.file.createDir(destination, folder, true);
|
||||||
|
|
||||||
|
// Folder created, add it to the destination path.
|
||||||
|
destination = CoreTextUtils.instance.concatenatePaths(destination, folder);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Extracts files from a ZIP archive.
|
||||||
|
*
|
||||||
|
* @param source Path to the source ZIP file.
|
||||||
|
* @param destination Destination folder.
|
||||||
|
* @param onProgress Optional callback to be called on progress update
|
||||||
|
* @return Promise that resolves with a number. 0 is success, -1 is error.
|
||||||
|
*/
|
||||||
|
async unzip(source: string, destination: string, onProgress?: (ev: {loaded: number; total: number}) => void): Promise<number> {
|
||||||
|
|
||||||
|
// Replace all %20 with spaces.
|
||||||
|
source = source.replace(/%20/g, ' ');
|
||||||
|
destination = destination.replace(/%20/g, ' ');
|
||||||
|
|
||||||
|
const sourceDir = source.substring(0, source.lastIndexOf('/'));
|
||||||
|
const sourceName = source.substr(source.lastIndexOf('/') + 1);
|
||||||
|
const zip = new JSZip();
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Read the file first.
|
||||||
|
const data = await this.file.readAsArrayBuffer(sourceDir, sourceName);
|
||||||
|
|
||||||
|
// Now load the file using the JSZip library.
|
||||||
|
await zip.loadAsync(data);
|
||||||
|
|
||||||
|
if (!zip.files || !Object.keys(zip.files).length) {
|
||||||
|
// Nothing to extract.
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
// First of all, create the directory where the files will be unzipped.
|
||||||
|
const destParent = destination.substring(0, destination.lastIndexOf('/'));
|
||||||
|
const destFolderName = destination.substr(destination.lastIndexOf('/') + 1);
|
||||||
|
|
||||||
|
await this.file.createDir(destParent, destFolderName, true);
|
||||||
|
|
||||||
|
const total = Object.keys(zip.files).length;
|
||||||
|
let loaded = 0;
|
||||||
|
|
||||||
|
await Promise.all(Object.keys(zip.files).map(async (name) => {
|
||||||
|
const file = zip.files[name];
|
||||||
|
|
||||||
|
if (!file.dir) {
|
||||||
|
// It's a file.
|
||||||
|
const fileDir = name.substring(0, name.lastIndexOf('/'));
|
||||||
|
const fileName = name.substr(name.lastIndexOf('/') + 1);
|
||||||
|
|
||||||
|
if (fileDir) {
|
||||||
|
// The file is in a subfolder, create it first.
|
||||||
|
await this.createDir(destination, fileDir);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Read the file contents as a Blob.
|
||||||
|
const fileData = await file.async('blob');
|
||||||
|
|
||||||
|
// File read and parent folder created, now write the file.
|
||||||
|
const parentFolder = CoreTextUtils.instance.concatenatePaths(destination, fileDir);
|
||||||
|
|
||||||
|
await this.file.writeFile(parentFolder, fileName, fileData, { replace: true });
|
||||||
|
} else {
|
||||||
|
// It's a folder, create it if it doesn't exist.
|
||||||
|
await this.createDir(destination, name);
|
||||||
|
}
|
||||||
|
|
||||||
|
// File unzipped, call the progress.
|
||||||
|
loaded++;
|
||||||
|
onProgress && onProgress({ loaded: loaded, total: total });
|
||||||
|
}));
|
||||||
|
|
||||||
|
return 0;
|
||||||
|
} catch (error) {
|
||||||
|
// Error.
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
Loading…
Reference in New Issue