diff --git a/README.md b/README.md index 1968adb9..2914cc06 100644 --- a/README.md +++ b/README.md @@ -7,9 +7,9 @@ A Cordova plugin to unzip files in Android and iOS. cordova plugin add cordova-plugin-zip ## Usage - +```javascript zip.unzip(, , , []); - +``` Both source and destination arguments can be URLs obtained from the HTML File interface or absolute paths to files on the device. @@ -19,16 +19,39 @@ success, or -1 on failure. The progressCallback argument is optional and will be executed whenever a new ZipEntry has been extracted. E.g.: - +```javascript var progressCallback = function(progressEvent) { $( "#progressbar" ).progressbar("value", Math.round((progressEvent.loaded / progressEvent.total) * 100)); }; - +``` The values `loaded` and `total` are the number of compressed bytes processed and total. Total is the file size of the zip file. +## Example +```typescript +const downZipUrl: string = downZipFileEntry.toInternalURL(); +const downUnzipDirectoryUrl: string = downUnzipDir.toInternalURL(); +zip.unzip( + downZipUrl, + downUnzipDirectoryUrl, + (result: CordovaZipPluginUnzipResult, errorMessage: string) => { + if (result == CordovaZipPluginUnzipResult.Success) { + resolve(); + } else { + this.log.error(errorMessage, 'an error occurred during unzip'); + reject('an error occurred during unzip: ' + errorMessage); + } + }, + event => onProgress(event.loaded, event.total)); +``` + ## Release Notes +### 3.2.0 +* Browser platform support +* Updated doc +* Pass error message to upper layer + ### 3.1.0 (Feb 23, 2016) * Updated SSZipArchive (ios lib) to 1.1 diff --git a/package-lock.json b/package-lock.json new file mode 100644 index 00000000..0054a58d --- /dev/null +++ b/package-lock.json @@ -0,0 +1,14 @@ +{ + "name": "cordova-plugin-zip", + "version": "3.2.0", + "lockfileVersion": 1, + "requires": true, + "dependencies": { + "typescript": { + "version": "3.9.5", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-3.9.5.tgz", + "integrity": "sha512-hSAifV3k+i6lEoCJ2k6R2Z/rp/H3+8sdmcn5NrS3/3kE7+RyZXm9aqvxWqjEXHAd8b0pShatpcdMTvEdvAJltQ==", + "dev": true + } + } +} diff --git a/package.json b/package.json index 94919f65..21ee3be6 100644 --- a/package.json +++ b/package.json @@ -1,33 +1,41 @@ { - "name": "cordova-plugin-zip", - "version": "3.1.0", - "description": "Unzips zip files", - "cordova": { - "id": "cordova-plugin-unzip", - "platforms": [ - "android", - "ios" - ] - }, - "repository": { - "type": "git", - "url": "https://github.com/MobileChromeApps/cordova-plugin-zip.git" - }, - "keywords": [ - "ecosystem:cordova", - "cordova-android", - "cordova-ios" - ], - "engines": [ - { - "name": "cordova", - "version": ">=3.3.0" + "name": "cordova-plugin-zip", + "version": "3.2.0", + "description": "Unzips zip files", + "cordova": { + "id": "cordova-plugin-unzip", + "platforms": [ + "android", + "ios", + "browser" + ] + }, + "repository": { + "type": "git", + "url": "https://github.com/MobileChromeApps/cordova-plugin-zip.git" + }, + "keywords": [ + "cordova", + "zip", + "unzip", + "ecosystem:cordova", + "cordova-android", + "cordova-ios", + "cordova-browser" + ], + "engines": [ + { + "name": "cordova", + "version": ">=3.3.0" + } + ], + "author": "", + "license": "BSD", + "bugs": { + "url": "https://github.com/MobileChromeApps/zip/issues" + }, + "homepage": "https://github.com/MobileChromeApps/zip", + "devDependencies": { + "typescript": "3.9.5" } - ], - "author": "", - "license": "BSD", - "bugs": { - "url": "https://github.com/MobileChromeApps/zip/issues" - }, - "homepage": "https://github.com/MobileChromeApps/zip" } diff --git a/plugin.xml b/plugin.xml index 746ea4fe..45c4ede3 100644 --- a/plugin.xml +++ b/plugin.xml @@ -2,7 +2,7 @@ + version="3.2.0"> @@ -15,6 +15,17 @@ + + + + + + + + + + + diff --git a/src/browser/FileUtil.ts b/src/browser/FileUtil.ts new file mode 100644 index 00000000..a7c79b86 --- /dev/null +++ b/src/browser/FileUtil.ts @@ -0,0 +1,304 @@ +var __CORDOVA_PLUGIN_UNZIP_LOG_DEBUG_ENABLED = false; +var __CORDOVA_PLUGIN_UNZIP_LOG_INFO_ENABLED = false; + +function logDebug(...messages: any[]): void { + if (__CORDOVA_PLUGIN_UNZIP_LOG_DEBUG_ENABLED) { + console.debug(...messages); + } +} + +function logInfo(...messages: any[]): void { + if (__CORDOVA_PLUGIN_UNZIP_LOG_INFO_ENABLED) { + console.info(...messages); + } +} + +class CordovaPluginFileUtils { + + static isFileError(error: any, requestedError: CordovaPluginFileUtils.FileErrors): boolean { + if (error.name && error.name == CordovaPluginFileUtils.FileErrors[requestedError]) { + return true; + } + + if (error.code && error.code == requestedError) { + return true; + } + + return false; + } + + static getFileEntry(path: string, parentDirectory: DirectoryEntry): Promise { + return new Promise((resolve, reject) => { + parentDirectory.getFile(path, {}, resolve, reject); + }); + } + + static resolveOrCreateDirectoryEntry(entryUrl: string): Promise { + return CordovaPluginFileUtils.resolveOrCreateEntry(entryUrl, true) as Promise; + } + + static resolveOrCreateFileEntry(entryUrl: string): Promise { + return CordovaPluginFileUtils.resolveOrCreateEntry(entryUrl, false) as Promise; + } + + static resolveEntry(entryUrl: string): Promise { + return new Promise((resolve, reject) => { + window.resolveLocalFileSystemURL(entryUrl, resolve, reject); + }) as Promise; + } + + static async resolveOrCreateEntry(entryUrl: string, directory: boolean): Promise { + let entry: DirectoryEntry | FileEntry; + try { + entry = await CordovaPluginFileUtils.resolveEntry(entryUrl); + } catch (e) { + console.error(e); + console.error(`cannot resolve directory entry at url ${entryUrl}`); + + let fileSystem: FileSystem; + if (entryUrl.indexOf('/temporary/') != -1) { + fileSystem = await CordovaPluginFileUtils.getFileSystem(CordovaPluginFileUtils.FileSystemType.TEMPORARY); + } else { + fileSystem = await CordovaPluginFileUtils.getFileSystem(); + } + let path: string = entryUrl; + if (entryUrl.indexOf('/temporary/') != -1) { + path = entryUrl.substring(entryUrl.indexOf('/temporary/') + '/temporary/'.length - 1); + } else if (entryUrl.indexOf('/persistent/') != -1) { + path = entryUrl.substring(entryUrl.indexOf('/persistent/') + '/persistent/'.length - 1); + } + + entry = await new Promise((resolve, reject) => { + if (directory) { + fileSystem.root.getDirectory(path, { create: true, exclusive: false }, resolve, reject); + } else { + fileSystem.root.getFile(path, { create: true, exclusive: true }, resolve, reject); + } + }); + } + + return entry; + } + + static getOrCreateChildDirectory(parent: DirectoryEntry, childDirPath: string): Promise { + let folders: string[] = childDirPath.split('/'); + return CordovaPluginFileUtils.getOrCreateDirectoryForPath(parent, folders.filter(folder => folder != '')); + } + + static async getOrCreateDirectoryForPath(parent: DirectoryEntry, pathEntries: string[]): Promise { + pathEntries = pathEntries.filter(pathEntry => pathEntry != ''); + + return new Promise((resolve, reject) => { + logDebug('resolving dir path', pathEntries); + + if (pathEntries.length == 0) { + return resolve(parent); + } + + // Throw out './' or '/' and move on to prevent something like '/foo/.//bar'. + if (pathEntries[0] == '.' || pathEntries[0] == '') { + pathEntries = pathEntries.slice(1); + } + + parent.getDirectory(pathEntries[0], { create: true }, (dirEntry: DirectoryEntry) => { + logDebug('directory ' + pathEntries[0] + ' available, remaining: ' + (pathEntries.length - 1)); + + // Recursively add the new subfolder (if we still have another to create). + if (pathEntries.length > 1) { + CordovaPluginFileUtils.getOrCreateDirectoryForPath(dirEntry, pathEntries.slice(1)) + .then(resolve) + .catch(reject); + } else { + resolve(dirEntry); + } + }, reject); + }); + } + + static getParent(entry: Entry): Promise { + return new Promise((resolve, reject) => { + entry.getParent(entry => resolve(entry as DirectoryEntry), reject); + }); + } + + static async exists(path: string, parentDirectory: DirectoryEntry): Promise { + try { + await CordovaPluginFileUtils.getFileEntry(path, parentDirectory); + return true; + } catch (error) { + if (CordovaPluginFileUtils.isFileError(error, CordovaPluginFileUtils.FileErrors.TypeMismatchError)) { + return true; + } + + if (CordovaPluginFileUtils.isFileError(error, CordovaPluginFileUtils.FileErrors.NotFoundError)) { + return false; + } + + throw error; + } + } + + static async getEntryTypeAtPath(path: string, parentDirectory: DirectoryEntry): Promise { + try { + await CordovaPluginFileUtils.getFileEntry(path, parentDirectory); + return CordovaPluginFileUtils.EntryType.File; + } catch (error) { + if (CordovaPluginFileUtils.isFileError(error, CordovaPluginFileUtils.FileErrors.TypeMismatchError)) { + return CordovaPluginFileUtils.EntryType.Directory; + } + + throw error; + } + } + + private static fileSystemsCache: { [type: number]: FileSystem } = {}; + static async getFileSystem(type: CordovaPluginFileUtils.FileSystemType = CordovaPluginFileUtils.FileSystemType.PERSISTENT): Promise { + if (CordovaPluginFileUtils.fileSystemsCache[type]) { + return CordovaPluginFileUtils.fileSystemsCache[type]; + } + + const requestFileSystem = window['webkitRequestFileSystem'] || window.requestFileSystem; + const storageInfo = navigator['webkitPersistentStorage'] || window['storageInfo']; + + logDebug(`zip plugin - requestFileSystem=${requestFileSystem} - storageInfo=${storageInfo}`); + + // request storage quota + const requestedBytes: number = (1000 * 1000000 /* ? x 1Mo */); + let grantedBytes: number = 0; + if (storageInfo != null) { + grantedBytes = await new Promise((resolve, reject) => { + storageInfo.requestQuota(requestedBytes, resolve, reject); + }); + } + logDebug('granted bytes: ' + grantedBytes); + + // request file system + if (!requestFileSystem) { + throw new Error('cannot access filesystem API'); + } + const fileSystem: FileSystem = await new Promise((resolve, reject) => { + requestFileSystem(type, grantedBytes, resolve, reject); + }); + logDebug('FileSystem ready: ' + fileSystem.name); + + CordovaPluginFileUtils.fileSystemsCache[type] = fileSystem; + + return fileSystem; + } + + static async listDirectoryContent(dir: DirectoryEntry, recursive: boolean = false): Promise { + const contentAvailable = new Promise((resolve, reject) => { + let dirReader = dir.createReader(); + let entries: Entry[] = []; + + let readEntries = () => { + dirReader.readEntries((results) => { + if (!results.length) { + resolve(entries); + } else { + entries = entries.concat(results); + readEntries(); + } + }, reject); + }; + + readEntries(); + }); + + const content: Entry[] = await contentAvailable; + + if (recursive) { + const recursiveChildren: Entry[] = []; + for (const directChild of content) { + if (directChild.isDirectory) { + recursiveChildren.push(...(await CordovaPluginFileUtils.listDirectoryContent(directChild, true))); + } + } + + content.push(...recursiveChildren); + } + + return content; + } + + static getRelativePath(baseDirectory: DirectoryEntry, file: Entry): string { + return file.fullPath // + .replace(new RegExp('^/?/?' + baseDirectory.fullPath + '/?', 'g'), '') // removes //basePath/ + .replace(/\/$/g, ''); // removes trailing / + } + + static async copyDirectoryWithOverwrite( + sourceDirectory: DirectoryEntry, + targetDirectory: DirectoryEntry, + move: boolean = false, + onProgress?: (loaded: number, total: number) => void) { + + const entries: Entry[] = await CordovaPluginFileUtils.listDirectoryContent(sourceDirectory, true); + + let i = 0; + for (const entry of entries) { + logInfo(`> copy ${entry.fullPath}`); + + let directoryToBeCopied: DirectoryEntry; + if (entry.isDirectory) { + directoryToBeCopied = entry as DirectoryEntry; + } else { + directoryToBeCopied = await CordovaPluginFileUtils.getParent(entry); + } + + logDebug(`directory to be copied ${directoryToBeCopied.fullPath}`); + + const directoryRelativePath: string = CordovaPluginFileUtils.getRelativePath(sourceDirectory, directoryToBeCopied); + let targetParentDirectory: DirectoryEntry = targetDirectory; + if (directoryToBeCopied.fullPath != sourceDirectory.fullPath) { + targetParentDirectory = await CordovaPluginFileUtils.getOrCreateChildDirectory(targetDirectory, directoryRelativePath); + } + logDebug('targetParentDirectory=' + targetDirectory.fullPath); + + if (!entry.isDirectory) { + await new Promise((resolve, reject) => { + if (move) { + logDebug(`move file ${entry.fullPath} to ${targetParentDirectory.fullPath}`); + entry.moveTo(targetParentDirectory, entry.name, resolve, reject); + } else { + logDebug(`copy file ${entry.fullPath} to ${targetParentDirectory.fullPath}`); + entry.copyTo(targetParentDirectory, entry.name, resolve, reject); + } + }); + + logInfo(`copied file: ${entry.fullPath} to ${targetParentDirectory.fullPath}`); + } + + onProgress(++i, entries.length); + } + + // remove source directory in move case + if (move) { + await CordovaPluginFileUtils.removeDirectory(sourceDirectory); + } + } + + static removeDirectory(directoryEntry: DirectoryEntry): Promise { + return new Promise((resolve, reject) => { + directoryEntry.removeRecursively(resolve, reject); + }); + } +} + +namespace CordovaPluginFileUtils { + + export enum FileSystemType { + TEMPORARY = window.TEMPORARY, + PERSISTENT = window.PERSISTENT + } + + export enum FileErrors { + TypeMismatchError = 11, + NotFoundError = 1 + } + + export enum EntryType { + File, + Directory + } +} \ No newline at end of file diff --git a/src/browser/README.md b/src/browser/README.md new file mode 100644 index 00000000..8219970c --- /dev/null +++ b/src/browser/README.md @@ -0,0 +1,35 @@ +# Cordova ZIP plugin - browser endpoint +Written in TypeScript, transpiled to JavaScript + +## Build +``` +npx tsc cordova-plugin-file.d.ts zip.js.d.ts FileUtil.ts ZipProxy.ts --target ES6 --outFile ZipProxy.js +``` + +## Integrate +Cordova ZIP plugin uses zip.js (https://github.com/gildas-lormeau/zip.js) to unzip, you have to include it in your app this way: +(your zip.js-bundle.js should at least include zip.js, inflate.js, deflate.js, zip-ext.js) + +```typescript + private async importBrowserZipSupport(): Promise { + + const zipPlugin = zip; + if (isBrowserPlatform()) { + console.info('browser platform: loading zip.js'); + // zip.js is required for browser platform (cordova-plugin-zip relies on it) + // it should be included by cordova plugin zip platform browser + const zipJsScript: HTMLScriptElement = document.createElement('script'); + const zipJsScriptLoaded = new Promise((resolve, reject) => { + zipJsScript.onload = () => resolve(); + }); + zipJsScript.src = 'path/to/zip.js-bundle.js'; + document.body.appendChild(zipJsScript); + + await zipJsScriptLoaded; + console.info('browser platform: zip.js loaded!'); + + window['zip'] = Object.assign(zipPlugin, window['zip']); + console.debug('browser platform: zip.js merged into zip'); + } +} +``` \ No newline at end of file diff --git a/src/browser/ZipProxy.js b/src/browser/ZipProxy.js new file mode 100644 index 00000000..d978b96c --- /dev/null +++ b/src/browser/ZipProxy.js @@ -0,0 +1,368 @@ +var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) { + function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); } + return new (P || (P = Promise))(function (resolve, reject) { + function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } } + function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } } + function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); } + step((generator = generator.apply(thisArg, _arguments || [])).next()); + }); +}; +var __CORDOVA_PLUGIN_UNZIP_LOG_DEBUG_ENABLED = false; +var __CORDOVA_PLUGIN_UNZIP_LOG_INFO_ENABLED = false; +function logDebug(...messages) { + if (__CORDOVA_PLUGIN_UNZIP_LOG_DEBUG_ENABLED) { + console.debug(...messages); + } +} +function logInfo(...messages) { + if (__CORDOVA_PLUGIN_UNZIP_LOG_INFO_ENABLED) { + console.info(...messages); + } +} +class CordovaPluginFileUtils { + static isFileError(error, requestedError) { + if (error.name && error.name == CordovaPluginFileUtils.FileErrors[requestedError]) { + return true; + } + if (error.code && error.code == requestedError) { + return true; + } + return false; + } + static getFileEntry(path, parentDirectory) { + return new Promise((resolve, reject) => { + parentDirectory.getFile(path, {}, resolve, reject); + }); + } + static resolveOrCreateDirectoryEntry(entryUrl) { + return CordovaPluginFileUtils.resolveOrCreateEntry(entryUrl, true); + } + static resolveOrCreateFileEntry(entryUrl) { + return CordovaPluginFileUtils.resolveOrCreateEntry(entryUrl, false); + } + static resolveEntry(entryUrl) { + return new Promise((resolve, reject) => { + window.resolveLocalFileSystemURL(entryUrl, resolve, reject); + }); + } + static resolveOrCreateEntry(entryUrl, directory) { + return __awaiter(this, void 0, void 0, function* () { + let entry; + try { + entry = yield CordovaPluginFileUtils.resolveEntry(entryUrl); + } + catch (e) { + console.error(e); + console.error(`cannot resolve directory entry at url ${entryUrl}`); + let fileSystem; + if (entryUrl.indexOf('/temporary/') != -1) { + fileSystem = yield CordovaPluginFileUtils.getFileSystem(CordovaPluginFileUtils.FileSystemType.TEMPORARY); + } + else { + fileSystem = yield CordovaPluginFileUtils.getFileSystem(); + } + let path = entryUrl; + if (entryUrl.indexOf('/temporary/') != -1) { + path = entryUrl.substring(entryUrl.indexOf('/temporary/') + '/temporary/'.length - 1); + } + else if (entryUrl.indexOf('/persistent/') != -1) { + path = entryUrl.substring(entryUrl.indexOf('/persistent/') + '/persistent/'.length - 1); + } + entry = yield new Promise((resolve, reject) => { + if (directory) { + fileSystem.root.getDirectory(path, { create: true, exclusive: false }, resolve, reject); + } + else { + fileSystem.root.getFile(path, { create: true, exclusive: true }, resolve, reject); + } + }); + } + return entry; + }); + } + static getOrCreateChildDirectory(parent, childDirPath) { + let folders = childDirPath.split('/'); + return CordovaPluginFileUtils.getOrCreateDirectoryForPath(parent, folders.filter(folder => folder != '')); + } + static getOrCreateDirectoryForPath(parent, pathEntries) { + return __awaiter(this, void 0, void 0, function* () { + pathEntries = pathEntries.filter(pathEntry => pathEntry != ''); + return new Promise((resolve, reject) => { + logDebug('resolving dir path', pathEntries); + if (pathEntries.length == 0) { + return resolve(parent); + } + // Throw out './' or '/' and move on to prevent something like '/foo/.//bar'. + if (pathEntries[0] == '.' || pathEntries[0] == '') { + pathEntries = pathEntries.slice(1); + } + parent.getDirectory(pathEntries[0], { create: true }, (dirEntry) => { + logDebug('directory ' + pathEntries[0] + ' available, remaining: ' + (pathEntries.length - 1)); + // Recursively add the new subfolder (if we still have another to create). + if (pathEntries.length > 1) { + CordovaPluginFileUtils.getOrCreateDirectoryForPath(dirEntry, pathEntries.slice(1)) + .then(resolve) + .catch(reject); + } + else { + resolve(dirEntry); + } + }, reject); + }); + }); + } + static getParent(entry) { + return new Promise((resolve, reject) => { + entry.getParent(entry => resolve(entry), reject); + }); + } + static exists(path, parentDirectory) { + return __awaiter(this, void 0, void 0, function* () { + try { + yield CordovaPluginFileUtils.getFileEntry(path, parentDirectory); + return true; + } + catch (error) { + if (CordovaPluginFileUtils.isFileError(error, CordovaPluginFileUtils.FileErrors.TypeMismatchError)) { + return true; + } + if (CordovaPluginFileUtils.isFileError(error, CordovaPluginFileUtils.FileErrors.NotFoundError)) { + return false; + } + throw error; + } + }); + } + static getEntryTypeAtPath(path, parentDirectory) { + return __awaiter(this, void 0, void 0, function* () { + try { + yield CordovaPluginFileUtils.getFileEntry(path, parentDirectory); + return CordovaPluginFileUtils.EntryType.File; + } + catch (error) { + if (CordovaPluginFileUtils.isFileError(error, CordovaPluginFileUtils.FileErrors.TypeMismatchError)) { + return CordovaPluginFileUtils.EntryType.Directory; + } + throw error; + } + }); + } + static getFileSystem(type = CordovaPluginFileUtils.FileSystemType.PERSISTENT) { + return __awaiter(this, void 0, void 0, function* () { + if (CordovaPluginFileUtils.fileSystemsCache[type]) { + return CordovaPluginFileUtils.fileSystemsCache[type]; + } + const requestFileSystem = window['webkitRequestFileSystem'] || window.requestFileSystem; + const storageInfo = navigator['webkitPersistentStorage'] || window['storageInfo']; + logDebug(`zip plugin - requestFileSystem=${requestFileSystem} - storageInfo=${storageInfo}`); + // request storage quota + const requestedBytes = (1000 * 1000000 /* ? x 1Mo */); + let grantedBytes = 0; + if (storageInfo != null) { + grantedBytes = yield new Promise((resolve, reject) => { + storageInfo.requestQuota(requestedBytes, resolve, reject); + }); + } + logDebug('granted bytes: ' + grantedBytes); + // request file system + if (!requestFileSystem) { + throw new Error('cannot access filesystem API'); + } + const fileSystem = yield new Promise((resolve, reject) => { + requestFileSystem(type, grantedBytes, resolve, reject); + }); + logDebug('FileSystem ready: ' + fileSystem.name); + CordovaPluginFileUtils.fileSystemsCache[type] = fileSystem; + return fileSystem; + }); + } + static listDirectoryContent(dir, recursive = false) { + return __awaiter(this, void 0, void 0, function* () { + const contentAvailable = new Promise((resolve, reject) => { + let dirReader = dir.createReader(); + let entries = []; + let readEntries = () => { + dirReader.readEntries((results) => { + if (!results.length) { + resolve(entries); + } + else { + entries = entries.concat(results); + readEntries(); + } + }, reject); + }; + readEntries(); + }); + const content = yield contentAvailable; + if (recursive) { + const recursiveChildren = []; + for (const directChild of content) { + if (directChild.isDirectory) { + recursiveChildren.push(...(yield CordovaPluginFileUtils.listDirectoryContent(directChild, true))); + } + } + content.push(...recursiveChildren); + } + return content; + }); + } + static getRelativePath(baseDirectory, file) { + return file.fullPath // + .replace(new RegExp('^/?/?' + baseDirectory.fullPath + '/?', 'g'), '') // removes //basePath/ + .replace(/\/$/g, ''); // removes trailing / + } + static copyDirectoryWithOverwrite(sourceDirectory, targetDirectory, move = false, onProgress) { + return __awaiter(this, void 0, void 0, function* () { + const entries = yield CordovaPluginFileUtils.listDirectoryContent(sourceDirectory, true); + let i = 0; + for (const entry of entries) { + logInfo(`> copy ${entry.fullPath}`); + let directoryToBeCopied; + if (entry.isDirectory) { + directoryToBeCopied = entry; + } + else { + directoryToBeCopied = yield CordovaPluginFileUtils.getParent(entry); + } + logDebug(`directory to be copied ${directoryToBeCopied.fullPath}`); + const directoryRelativePath = CordovaPluginFileUtils.getRelativePath(sourceDirectory, directoryToBeCopied); + let targetParentDirectory = targetDirectory; + if (directoryToBeCopied.fullPath != sourceDirectory.fullPath) { + targetParentDirectory = yield CordovaPluginFileUtils.getOrCreateChildDirectory(targetDirectory, directoryRelativePath); + } + logDebug('targetParentDirectory=' + targetDirectory.fullPath); + if (!entry.isDirectory) { + yield new Promise((resolve, reject) => { + if (move) { + logDebug(`move file ${entry.fullPath} to ${targetParentDirectory.fullPath}`); + entry.moveTo(targetParentDirectory, entry.name, resolve, reject); + } + else { + logDebug(`copy file ${entry.fullPath} to ${targetParentDirectory.fullPath}`); + entry.copyTo(targetParentDirectory, entry.name, resolve, reject); + } + }); + logInfo(`copied file: ${entry.fullPath} to ${targetParentDirectory.fullPath}`); + } + onProgress(++i, entries.length); + } + // remove source directory in move case + if (move) { + yield CordovaPluginFileUtils.removeDirectory(sourceDirectory); + } + }); + } + static removeDirectory(directoryEntry) { + return new Promise((resolve, reject) => { + directoryEntry.removeRecursively(resolve, reject); + }); + } +} +CordovaPluginFileUtils.fileSystemsCache = {}; +(function (CordovaPluginFileUtils) { + let FileSystemType; + (function (FileSystemType) { + FileSystemType[FileSystemType["TEMPORARY"] = window.TEMPORARY] = "TEMPORARY"; + FileSystemType[FileSystemType["PERSISTENT"] = window.PERSISTENT] = "PERSISTENT"; + })(FileSystemType = CordovaPluginFileUtils.FileSystemType || (CordovaPluginFileUtils.FileSystemType = {})); + let FileErrors; + (function (FileErrors) { + FileErrors[FileErrors["TypeMismatchError"] = 11] = "TypeMismatchError"; + FileErrors[FileErrors["NotFoundError"] = 1] = "NotFoundError"; + })(FileErrors = CordovaPluginFileUtils.FileErrors || (CordovaPluginFileUtils.FileErrors = {})); + let EntryType; + (function (EntryType) { + EntryType[EntryType["File"] = 0] = "File"; + EntryType[EntryType["Directory"] = 1] = "Directory"; + })(EntryType = CordovaPluginFileUtils.EntryType || (CordovaPluginFileUtils.EntryType = {})); +})(CordovaPluginFileUtils || (CordovaPluginFileUtils = {})); +function unzipEntry(entry, outputDirectoryEntry) { + return __awaiter(this, void 0, void 0, function* () { + logDebug(`extracting ${entry.filename} to ${outputDirectoryEntry.fullPath}`); + let isDirectory = entry.filename.charAt(entry.filename.length - 1) == '/'; + let directoryPathEntries = entry.filename.split('/').filter(pathEntry => !!pathEntry); + if (!isDirectory) { + directoryPathEntries.splice(directoryPathEntries.length - 1, 1); + } + logInfo('directoryPathEntries=' + directoryPathEntries.join(', ')); + let targetDirectory = outputDirectoryEntry; + if (directoryPathEntries.length > 0) { + targetDirectory = yield CordovaPluginFileUtils.getOrCreateDirectoryForPath(outputDirectoryEntry, directoryPathEntries); + } + logInfo('targetDirectory=' + targetDirectory.fullPath); + if (!isDirectory) { + logDebug('adding file (get file): ' + entry.filename); + const targetFileEntry = yield new Promise((resolve, reject) => { + outputDirectoryEntry.getFile(entry.filename, { create: true, exclusive: false }, resolve, reject); + }); + logDebug('adding file (write file): ' + entry.filename); + yield new Promise((resolve, reject) => { + entry.getData(new zip.FileWriter(targetFileEntry), resolve, (progress, total) => { + logDebug(`${entry.filename}: ${progress} / ${total}`); + }); + }); + logDebug('added file: ' + entry.filename); + } + }); +} +function unzip(zipFileUrl, outputDirectoryUrl, successCallback, errorCallback) { + return __awaiter(this, void 0, void 0, function* () { + zip.useWebWorkers = false; + function onProgress(loaded, total) { + successCallback({ loaded, total }, { keepCallback: true }); + } + try { + if (!zip) { + throw new Error('zip.js not available, please import it: https://gildas-lormeau.github.io/zip.js'); + } + logInfo(`unzipping ${zipFileUrl} to ${outputDirectoryUrl}`); + logDebug(`retrieving output directory: ${outputDirectoryUrl}`); + const outputDirectoryEntry = yield CordovaPluginFileUtils.resolveOrCreateDirectoryEntry(outputDirectoryUrl); + logDebug(`output directory entry: ${outputDirectoryEntry}`); + logDebug(`retrieving zip file: ${zipFileUrl}`); + let zipEntry = yield CordovaPluginFileUtils.resolveOrCreateFileEntry(zipFileUrl); + logDebug(`zip file entry: ${zipEntry}`); + const zipBlob = yield new Promise((resolve, reject) => { + zipEntry.file(resolve, reject); + }); + logInfo(`open reader on zip: ${zipFileUrl}`); + zip.createReader(new zip.BlobReader(zipBlob), (zipReader) => { + logDebug(`reader opened on zip: ${zipFileUrl}`); + zipReader.getEntries((zipEntries) => __awaiter(this, void 0, void 0, function* () { + logDebug(`entries read: ${zipFileUrl}`); + onProgress(0, zipEntries.length); + try { + let i = 0; + for (const entry of zipEntries) { + yield unzipEntry(entry, outputDirectoryEntry); + onProgress(++i, zipEntries.length); + } + zipReader.close(() => { + logInfo(`unzip OK from ${zipFileUrl} to ${outputDirectoryUrl}`); + successCallback({ + total: zipEntries.length + }); + }); + } + catch (e) { + console.error(e, `error while unzipping ${zipFileUrl} to ${outputDirectoryUrl}`); + zipReader.close(); + errorCallback(e); + } + })); + }, errorCallback); + } + catch (e) { + console.error(e, `error while unzipping ${zipFileUrl} to ${outputDirectoryUrl}`); + errorCallback(e); + } + }); +} +module.exports = { + unzip: function (successCallback, errorCallback, args) { + const [zipFileUrl, outputDirectoryUrl] = args; + unzip(zipFileUrl, outputDirectoryUrl, successCallback, errorCallback); + } +}; +require("cordova/exec/proxy").add("Zip", module.exports); diff --git a/src/browser/ZipProxy.ts b/src/browser/ZipProxy.ts new file mode 100644 index 00000000..b9101940 --- /dev/null +++ b/src/browser/ZipProxy.ts @@ -0,0 +1,120 @@ + +async function unzipEntry(entry: zip.Entry, outputDirectoryEntry: DirectoryEntry) { + logDebug(`extracting ${entry.filename} to ${outputDirectoryEntry.fullPath}`); + let isDirectory = entry.filename.charAt(entry.filename.length - 1) == '/'; + + let directoryPathEntries: string[] = entry.filename.split('/').filter(pathEntry => !!pathEntry); + if (!isDirectory) { + directoryPathEntries.splice(directoryPathEntries.length - 1, 1); + } + logInfo('directoryPathEntries=' + directoryPathEntries.join(', ')); + + let targetDirectory: DirectoryEntry = outputDirectoryEntry; + if (directoryPathEntries.length > 0) { + targetDirectory = await CordovaPluginFileUtils.getOrCreateDirectoryForPath(outputDirectoryEntry, directoryPathEntries); + } + logInfo('targetDirectory=' + targetDirectory.fullPath); + + if (!isDirectory) { + logDebug('adding file (get file): ' + entry.filename); + const targetFileEntry = await new Promise((resolve, reject) => { + outputDirectoryEntry.getFile(entry.filename, { create: true, exclusive: false }, resolve, reject); + }); + logDebug('adding file (write file): ' + entry.filename); + await new Promise((resolve, reject) => { + entry.getData(new zip.FileWriter(targetFileEntry), resolve, (progress, total) => { + logDebug(`${entry.filename}: ${progress} / ${total}`); + }); + }); + logDebug('added file: ' + entry.filename); + } +} + +interface SuccessCallback { + (event: { loaded?: number, total: number }, options?): void; +} + +async function unzip( + zipFileUrl: string, + outputDirectoryUrl: string, + successCallback: SuccessCallback, + errorCallback) { + + zip.useWebWorkers = false; + + function onProgress(loaded: number, total: number) { + successCallback( + { loaded, total }, + { keepCallback: true }); + } + + try { + + if (!zip) { + throw new Error('zip.js not available, please import it: https://gildas-lormeau.github.io/zip.js'); + } + + logInfo(`unzipping ${zipFileUrl} to ${outputDirectoryUrl}`); + + logDebug(`retrieving output directory: ${outputDirectoryUrl}`); + const outputDirectoryEntry: DirectoryEntry = await CordovaPluginFileUtils.resolveOrCreateDirectoryEntry(outputDirectoryUrl); + logDebug(`output directory entry: ${outputDirectoryEntry}`); + + logDebug(`retrieving zip file: ${zipFileUrl}`); + let zipEntry: FileEntry = await CordovaPluginFileUtils.resolveOrCreateFileEntry(zipFileUrl); + logDebug(`zip file entry: ${zipEntry}`); + + const zipBlob: Blob = await new Promise((resolve, reject) => { + zipEntry.file(resolve, reject); + }); + + logInfo(`open reader on zip: ${zipFileUrl}`); + zip.createReader(new zip.BlobReader(zipBlob), (zipReader) => { + + logDebug(`reader opened on zip: ${zipFileUrl}`); + zipReader.getEntries(async (zipEntries) => { + + logDebug(`entries read: ${zipFileUrl}`); + + onProgress(0, zipEntries.length); + + try { + + let i = 0; + for (const entry of zipEntries) { + await unzipEntry(entry, outputDirectoryEntry); + + onProgress(++i, zipEntries.length); + } + + zipReader.close(() => { + logInfo(`unzip OK from ${zipFileUrl} to ${outputDirectoryUrl}`); + successCallback({ + total: zipEntries.length + }); + }); + + } catch (e) { + console.error(e, `error while unzipping ${zipFileUrl} to ${outputDirectoryUrl}`); + zipReader.close(); + errorCallback(e); + } + }); + }, errorCallback); + + } catch (e) { + console.error(e, `error while unzipping ${zipFileUrl} to ${outputDirectoryUrl}`); + errorCallback(e); + } +} + +declare var module; +declare var require; + +module.exports = { + unzip: function (successCallback, errorCallback, args) { + const [zipFileUrl, outputDirectoryUrl] = args; + unzip(zipFileUrl, outputDirectoryUrl, successCallback, errorCallback); + } +}; +require("cordova/exec/proxy").add("Zip", module.exports); diff --git a/src/browser/cordova-plugin-file.d.ts b/src/browser/cordova-plugin-file.d.ts new file mode 100644 index 00000000..c748e3da --- /dev/null +++ b/src/browser/cordova-plugin-file.d.ts @@ -0,0 +1,378 @@ +// Type definitions for Apache Cordova File System plugin +// Project: https://github.com/apache/cordova-plugin-file +// Definitions by: Microsoft Open Technologies Inc +// Definitions: https://github.com/DefinitelyTyped/DefinitelyTyped +// +// Copyright (c) Microsoft Open Technologies, Inc. +// Licensed under the MIT license. + +interface Window { + /** + * Requests a filesystem in which to store application data. + * @param type Whether the filesystem requested should be persistent, as defined above. Use one of TEMPORARY or PERSISTENT. + * @param size This is an indicator of how much storage space, in bytes, the application expects to need. + * @param successCallback The callback that is called when the user agent provides a filesystem. + * @param errorCallback A callback that is called when errors happen, or when the request to obtain the filesystem is denied. + */ + requestFileSystem( + type: LocalFileSystem, + size: number, + successCallback: (fileSystem: FileSystem) => void, + errorCallback?: (fileError: FileError) => void): void; + /** + * Look up file system Entry referred to by local URL. + * @param string url URL referring to a local file or directory + * @param successCallback invoked with Entry object corresponding to URL + * @param errorCallback invoked if error occurs retrieving file system entry + */ + resolveLocalFileSystemURL(url: string, + successCallback: (entry: Entry) => void, + errorCallback?: (error: FileError) => void): void; + /** + * Look up file system Entry referred to by local URI. + * @param string uri URI referring to a local file or directory + * @param successCallback invoked with Entry object corresponding to URI + * @param errorCallback invoked if error occurs retrieving file system entry + */ + resolveLocalFileSystemURI(uri: string, + successCallback: (entry: Entry) => void, + errorCallback?: (error: FileError) => void): void; + TEMPORARY: number; + PERSISTENT: number; +} + +/** This interface represents a file system. */ +interface FileSystem { + /* The name of the file system, unique across the list of exposed file systems. */ + name: string; + /** The root directory of the file system. */ + root: DirectoryEntry; +} + +/** + * An abstract interface representing entries in a file system, + * each of which may be a File or DirectoryEntry. + */ +interface Entry { + /** Entry is a file. */ + isFile: boolean; + /** Entry is a directory. */ + isDirectory: boolean; + /** The name of the entry, excluding the path leading to it. */ + name: string; + /** The full absolute path from the root to the entry. */ + fullPath: string; + /** The file system on which the entry resides. */ + fileSystem: FileSystem; + nativeURL: string; + /** + * Look up metadata about this entry. + * @param successCallback A callback that is called with the time of the last modification. + * @param errorCallback A callback that is called when errors happen. + */ + getMetadata( + successCallback: (metadata: Metadata) => void, + errorCallback?: (error: FileError) => void): void; + /** + * Move an entry to a different location on the file system. It is an error to try to: + * move a directory inside itself or to any child at any depth;move an entry into its parent if a name different from its current one isn't provided; + * move a file to a path occupied by a directory; + * move a directory to a path occupied by a file; + * move any element to a path occupied by a directory which is not empty. + * A move of a file on top of an existing file must attempt to delete and replace that file. + * A move of a directory on top of an existing empty directory must attempt to delete and replace that directory. + * @param parent The directory to which to move the entry. + * @param newName The new name of the entry. Defaults to the Entry's current name if unspecified. + * @param successCallback A callback that is called with the Entry for the new location. + * @param errorCallback A callback that is called when errors happen. + */ + moveTo(parent: DirectoryEntry, + newName?: string, + successCallback?: (entry: Entry) => void, + errorCallback?: (error: FileError) => void): void; + /** + * Copy an entry to a different location on the file system. It is an error to try to: + * copy a directory inside itself or to any child at any depth; + * copy an entry into its parent if a name different from its current one isn't provided; + * copy a file to a path occupied by a directory; + * copy a directory to a path occupied by a file; + * copy any element to a path occupied by a directory which is not empty. + * A copy of a file on top of an existing file must attempt to delete and replace that file. + * A copy of a directory on top of an existing empty directory must attempt to delete and replace that directory. + * Directory copies are always recursive--that is, they copy all contents of the directory. + * @param parent The directory to which to move the entry. + * @param newName The new name of the entry. Defaults to the Entry's current name if unspecified. + * @param successCallback A callback that is called with the Entry for the new object. + * @param errorCallback A callback that is called when errors happen. + */ + copyTo(parent: DirectoryEntry, + newName?: string, + successCallback?: (entry: Entry) => void, + errorCallback?: (error: FileError) => void): void; + /** + * Returns a URL that can be used as the src attribute of a