Vmeda.Online/src/app/services/file.ts

1248 lines
43 KiB
TypeScript
Raw Normal View History

// (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 { FileEntry, DirectoryEntry, Entry, Metadata, IFile } from '@ionic-native/file';
import { CoreApp } from '@services/app';
import { CoreWSExternalFile } from '@services/ws';
import { CoreMimetypeUtils } from '@services/utils/mimetype';
import { CoreTextUtils } from '@services/utils/text';
import { CoreUtils } from '@services/utils/utils';
import { CoreConstants } from '@core/constants';
import { CoreError } from '@classes/errors/error';
import { CoreLogger } from '@singletons/logger';
import { makeSingleton, File, Zip, Platform, WebView } from '@singletons/core.singletons';
/**
* Progress event used when writing a file data into a file.
*/
export type CoreFileProgressEvent = {
/**
* Whether the values are reliabñe.
*/
lengthComputable?: boolean;
/**
* Number of treated bytes.
*/
loaded?: number;
/**
* Total of bytes.
*/
total?: number;
};
/**
* Progress function.
*/
export type CoreFileProgressFunction = (event: CoreFileProgressEvent) => void;
/**
* Constants to define the format to read a file.
*/
export const enum CoreFileFormat {
FORMATTEXT = 0,
FORMATDATAURL = 1,
FORMATBINARYSTRING = 2,
FORMATARRAYBUFFER = 3,
FORMATJSON = 4,
}
/**
* Factory to interact with the file system.
*/
@Injectable()
export class CoreFileProvider {
// Formats to read a file.
static readonly FORMATTEXT = CoreFileFormat.FORMATTEXT;
static readonly FORMATDATAURL = CoreFileFormat.FORMATDATAURL;
static readonly FORMATBINARYSTRING = CoreFileFormat.FORMATBINARYSTRING;
static readonly FORMATARRAYBUFFER = CoreFileFormat.FORMATARRAYBUFFER;
static readonly FORMATJSON = CoreFileFormat.FORMATJSON;
// Folders.
static readonly SITESFOLDER = 'sites';
static readonly TMPFOLDER = 'tmp';
static readonly CHUNK_SIZE = 1048576; // 1 MB. Same chunk size as Ionic Native.
protected logger: CoreLogger;
protected initialized = false;
protected basePath = '';
protected isHTMLAPI = false;
constructor() {
this.logger = CoreLogger.getInstance('CoreFileProvider');
// @todo: Check if redefining FileReader getters and setters is still needed in Android.
}
/**
* Sets basePath to use with HTML API. Reserved for core use.
*
* @param path Base path to use.
*/
setHTMLBasePath(path: string): void {
this.isHTMLAPI = true;
this.basePath = path;
}
/**
* Checks if we're using HTML API.
*
* @return True if uses HTML API, false otherwise.
*/
usesHTMLAPI(): boolean {
return this.isHTMLAPI;
}
/**
* Initialize basePath based on the OS if it's not initialized already.
*
* @return Promise to be resolved when the initialization is finished.
*/
async init(): Promise<void> {
if (this.initialized) {
return;
}
await Platform.instance.ready();
if (CoreApp.instance.isAndroid()) {
this.basePath = File.instance.externalApplicationStorageDirectory || this.basePath;
} else if (CoreApp.instance.isIOS()) {
this.basePath = File.instance.documentsDirectory || this.basePath;
} else if (!this.isAvailable() || this.basePath === '') {
this.logger.error('Error getting device OS.');
return Promise.reject(new CoreError('Error getting device OS to initialize file system.'));
}
this.initialized = true;
this.logger.debug('FS initialized: ' + this.basePath);
}
/**
* Check if the plugin is available.
*
* @return Whether the plugin is available.
*/
isAvailable(): boolean {
return typeof window.resolveLocalFileSystemURL !== 'undefined';
}
/**
* Get a file.
*
* @param path Relative path to the file.
* @return Promise resolved when the file is retrieved.
*/
getFile(path: string): Promise<FileEntry> {
return this.init().then(() => {
this.logger.debug('Get file: ' + path);
return File.instance.resolveLocalFilesystemUrl(this.addBasePathIfNeeded(path));
}).then((entry) => <FileEntry> entry);
}
/**
* Get a directory.
*
* @param path Relative path to the directory.
* @return Promise resolved when the directory is retrieved.
*/
getDir(path: string): Promise<DirectoryEntry> {
return this.init().then(() => {
this.logger.debug('Get directory: ' + path);
return File.instance.resolveDirectoryUrl(this.addBasePathIfNeeded(path));
});
}
/**
* Get site folder path.
*
* @param siteId Site ID.
* @return Site folder path.
*/
getSiteFolder(siteId: string): string {
return CoreFileProvider.SITESFOLDER + '/' + siteId;
}
/**
* Create a directory or a file.
*
* @param isDirectory True if a directory should be created, false if it should create a file.
* @param path Relative path to the dir/file.
* @param failIfExists True if it should fail if the dir/file exists, false otherwise.
* @param base Base path to create the dir/file in. If not set, use basePath.
* @return Promise to be resolved when the dir/file is created.
*/
protected async create(
isDirectory: boolean,
path: string,
failIfExists?: boolean,
base?: string,
): Promise<FileEntry | DirectoryEntry> {
await this.init();
// Remove basePath if it's in the path.
path = this.removeStartingSlash(path.replace(this.basePath, ''));
base = base || this.basePath;
if (path.indexOf('/') == -1) {
if (isDirectory) {
this.logger.debug('Create dir ' + path + ' in ' + base);
return File.instance.createDir(base, path, !failIfExists);
} else {
this.logger.debug('Create file ' + path + ' in ' + base);
return File.instance.createFile(base, path, !failIfExists);
}
} else {
// The file plugin doesn't allow creating more than 1 level at a time (e.g. tmp/folder).
// We need to create them 1 by 1.
const firstDir = path.substr(0, path.indexOf('/'));
const restOfPath = path.substr(path.indexOf('/') + 1);
this.logger.debug('Create dir ' + firstDir + ' in ' + base);
const newDirEntry = await File.instance.createDir(base, firstDir, true);
return this.create(isDirectory, restOfPath, failIfExists, newDirEntry.toURL());
}
}
/**
* Create a directory.
*
* @param path Relative path to the directory.
* @param failIfExists True if it should fail if the directory exists, false otherwise.
* @return Promise to be resolved when the directory is created.
*/
async createDir(path: string, failIfExists?: boolean): Promise<DirectoryEntry> {
const entry = <DirectoryEntry> await this.create(true, path, failIfExists);
return entry;
}
/**
* Create a file.
*
* @param path Relative path to the file.
* @param failIfExists True if it should fail if the file exists, false otherwise..
* @return Promise to be resolved when the file is created.
*/
async createFile(path: string, failIfExists?: boolean): Promise<FileEntry> {
const entry = <FileEntry> await this.create(true, path, failIfExists);
return entry;
}
/**
* Removes a directory and all its contents.
*
* @param path Relative path to the directory.
* @return Promise to be resolved when the directory is deleted.
*/
async removeDir(path: string): Promise<void> {
await this.init();
// Remove basePath if it's in the path.
path = this.removeStartingSlash(path.replace(this.basePath, ''));
this.logger.debug('Remove directory: ' + path);
await File.instance.removeRecursively(this.basePath, path);
}
/**
* Removes a file and all its contents.
*
* @param path Relative path to the file.
* @return Promise to be resolved when the file is deleted.
*/
async removeFile(path: string): Promise<void> {
await this.init();
// Remove basePath if it's in the path.
path = this.removeStartingSlash(path.replace(this.basePath, ''));
this.logger.debug('Remove file: ' + path);
try {
await File.instance.removeFile(this.basePath, path);
} catch (error) {
// The delete can fail if the path has encoded characters. Try again if that's the case.
const decodedPath = decodeURI(path);
if (decodedPath != path) {
await File.instance.removeFile(this.basePath, decodedPath);
} else {
throw error;
}
}
}
/**
* Removes a file given its FileEntry.
*
* @param fileEntry File Entry.
* @return Promise resolved when the file is deleted.
*/
removeFileByFileEntry(entry: Entry): Promise<void> {
return new Promise((resolve, reject) => entry.remove(resolve, reject));
}
/**
* Retrieve the contents of a directory (not subdirectories).
*
* @param path Relative path to the directory.
* @return Promise to be resolved when the contents are retrieved.
*/
async getDirectoryContents(path: string): Promise<(FileEntry | DirectoryEntry)[]> {
await this.init();
// Remove basePath if it's in the path.
path = this.removeStartingSlash(path.replace(this.basePath, ''));
this.logger.debug('Get contents of dir: ' + path);
const result = await File.instance.listDir(this.basePath, path);
return <(FileEntry | DirectoryEntry)[]> result;
}
/**
* Type guard to check if the param is a DirectoryEntry.
*
* @param entry Param to check.
* @return Whether the param is a DirectoryEntry.
*/
protected isDirectoryEntry(entry: FileEntry | DirectoryEntry): entry is DirectoryEntry {
return entry.isDirectory === true;
}
/**
* Calculate the size of a directory or a file.
*
* @param entry Directory or file.
* @return Promise to be resolved when the size is calculated.
*/
protected getSize(entry: DirectoryEntry | FileEntry): Promise<number> {
return new Promise<number>((resolve, reject) => {
if (this.isDirectoryEntry(entry)) {
const directoryReader = entry.createReader();
directoryReader.readEntries(async (entries: (DirectoryEntry | FileEntry)[]) => {
const promises: Promise<number>[] = [];
for (let i = 0; i < entries.length; i++) {
promises.push(this.getSize(entries[i]));
}
try {
const sizes = await Promise.all(promises);
let directorySize = 0;
for (let i = 0; i < sizes.length; i++) {
const fileSize = Number(sizes[i]);
if (isNaN(fileSize)) {
reject();
return;
}
directorySize += fileSize;
}
resolve(directorySize);
} catch (error) {
reject(error);
}
}, reject);
} else {
entry.file((file) => {
resolve(file.size);
}, reject);
}
});
}
/**
* Calculate the size of a directory.
*
* @param path Relative path to the directory.
* @return Promise to be resolved when the size is calculated.
*/
getDirectorySize(path: string): Promise<number> {
// Remove basePath if it's in the path.
path = this.removeStartingSlash(path.replace(this.basePath, ''));
this.logger.debug('Get size of dir: ' + path);
return this.getDir(path).then((dirEntry) => this.getSize(dirEntry));
}
/**
* Calculate the size of a file.
*
* @param path Relative path to the file.
* @return Promise to be resolved when the size is calculated.
*/
getFileSize(path: string): Promise<number> {
// Remove basePath if it's in the path.
path = this.removeStartingSlash(path.replace(this.basePath, ''));
this.logger.debug('Get size of file: ' + path);
return this.getFile(path).then((fileEntry) => this.getSize(fileEntry));
}
/**
* Get file object from a FileEntry.
*
* @param path Relative path to the file.
* @return Promise to be resolved when the file is retrieved.
*/
getFileObjectFromFileEntry(entry: FileEntry): Promise<IFile> {
return new Promise((resolve, reject): void => {
this.logger.debug('Get file object of: ' + entry.fullPath);
entry.file(resolve, reject);
});
}
/**
* Calculate the free space in the disk.
* Please notice that this function isn't reliable and it's not documented in the Cordova File plugin.
*
* @return Promise resolved with the estimated free space in bytes.
*/
calculateFreeSpace(): Promise<number> {
return File.instance.getFreeDiskSpace().then((size) => {
if (CoreApp.instance.isIOS()) {
// In iOS the size is in bytes.
return Number(size);
}
// The size is in KB, convert it to bytes.
return Number(size) * 1024;
});
}
/**
* Normalize a filename that usually comes URL encoded.
*
* @param filename The file name.
* @return The file name normalized.
*/
normalizeFileName(filename: string): string {
filename = CoreTextUtils.instance.decodeURIComponent(filename);
return filename;
}
/**
* Read a file from local file system.
*
* @param path Relative path to the file.
* @param format Format to read the file.
* @return Promise to be resolved when the file is read.
*/
readFile(path: string, format: CoreFileFormat = CoreFileProvider.FORMATTEXT): Promise<string | ArrayBuffer | unknown> {
// Remove basePath if it's in the path.
path = this.removeStartingSlash(path.replace(this.basePath, ''));
this.logger.debug('Read file ' + path + ' with format ' + format);
switch (format) {
case CoreFileProvider.FORMATDATAURL:
return File.instance.readAsDataURL(this.basePath, path);
case CoreFileProvider.FORMATBINARYSTRING:
return File.instance.readAsBinaryString(this.basePath, path);
case CoreFileProvider.FORMATARRAYBUFFER:
return File.instance.readAsArrayBuffer(this.basePath, path);
case CoreFileProvider.FORMATJSON:
return File.instance.readAsText(this.basePath, path).then((text) => {
const parsed = CoreTextUtils.instance.parseJSON(text, null);
if (parsed == null && text != null) {
throw new CoreError('Error parsing JSON file: ' + path);
}
return parsed;
});
default:
return File.instance.readAsText(this.basePath, path);
}
}
/**
* Read file contents from a file data object.
*
* @param fileData File's data.
* @param format Format to read the file.
* @return Promise to be resolved when the file is read.
*/
readFileData(fileData: IFile, format: CoreFileFormat = CoreFileProvider.FORMATTEXT): Promise<string | ArrayBuffer | unknown> {
format = format || CoreFileProvider.FORMATTEXT;
this.logger.debug('Read file from file data with format ' + format);
return new Promise((resolve, reject): void => {
const reader = new FileReader();
reader.onloadend = (event): void => {
if (event.target?.result !== undefined && event.target.result !== null) {
if (format == CoreFileProvider.FORMATJSON) {
// Convert to object.
const parsed = CoreTextUtils.instance.parseJSON(<string> event.target.result, null);
if (parsed == null) {
reject('Error parsing JSON file.');
}
resolve(parsed);
} else {
resolve(event.target.result);
}
} else if (event.target?.error !== undefined && event.target.error !== null) {
reject(event.target.error);
} else {
reject({ code: null, message: 'READER_ONLOADEND_ERR' });
}
};
// Check if the load starts. If it doesn't start in 3 seconds, reject.
// Sometimes in Android the read doesn't start for some reason, so the promise never finishes.
let hasStarted = false;
reader.onloadstart = () => {
hasStarted = true;
};
setTimeout(() => {
if (!hasStarted) {
reject('Upload cannot start.');
}
}, 3000);
switch (format) {
case CoreFileProvider.FORMATDATAURL:
reader.readAsDataURL(fileData);
break;
case CoreFileProvider.FORMATBINARYSTRING:
reader.readAsBinaryString(fileData);
break;
case CoreFileProvider.FORMATARRAYBUFFER:
reader.readAsArrayBuffer(fileData);
break;
default:
reader.readAsText(fileData);
}
});
}
/**
* Writes some data in a file.
*
* @param path Relative path to the file.
* @param data Data to write.
* @param append Whether to append the data to the end of the file.
* @return Promise to be resolved when the file is written.
*/
async writeFile(path: string, data: string | Blob, append?: boolean): Promise<FileEntry> {
await this.init();
// Remove basePath if it's in the path.
path = this.removeStartingSlash(path.replace(this.basePath, ''));
this.logger.debug('Write file: ' + path);
// Create file (and parent folders) to prevent errors.
const fileEntry = await this.createFile(path);
if (this.isHTMLAPI && !CoreApp.instance.isDesktop() &&
(typeof data == 'string' || data.toString() == '[object ArrayBuffer]')) {
// We need to write Blobs.
const extension = CoreMimetypeUtils.instance.getFileExtension(path);
const type = extension ? CoreMimetypeUtils.instance.getMimeType(extension) : '';
data = new Blob([data], { type: type || 'text/plain' });
}
await File.instance.writeFile(this.basePath, path, data, { replace: !append, append: !!append });
return fileEntry;
}
/**
* Write some file data into a filesystem file.
* It's done in chunks to prevent crashing the app for big files.
* Please notice Ionic Native writeFile function already splits by chunks, but it doesn't have an onProgress function.
*
* @param file The data to write.
* @param path Path where to store the data.
* @param onProgress Function to call on progress.
* @param offset Offset where to start reading from.
* @param append Whether to append the data to the end of the file.
* @return Promise resolved when done.
*/
async writeFileDataInFile(
file: Blob,
path: string,
onProgress?: CoreFileProgressFunction,
offset: number = 0,
append?: boolean,
): Promise<FileEntry> {
offset = offset || 0;
try {
// Get the chunk to write.
const chunk = file.slice(offset, Math.min(offset + CoreFileProvider.CHUNK_SIZE, file.size));
const fileEntry = await this.writeFile(path, chunk, append);
offset += CoreFileProvider.CHUNK_SIZE;
onProgress && onProgress({
lengthComputable: true,
loaded: offset,
total: file.size,
});
if (offset >= file.size) {
// Done, stop.
return fileEntry;
}
// Read the next chunk.
return this.writeFileDataInFile(file, path, onProgress, offset, true);
} catch (error) {
if (error && error.target && error.target.error) {
// Error returned by the writer, throw the "real" error.
throw error.target.error;
}
throw error;
}
}
/**
* Gets a file that might be outside the app's folder.
*
* @param fullPath Absolute path to the file.
* @return Promise to be resolved when the file is retrieved.
*/
getExternalFile(fullPath: string): Promise<FileEntry> {
return File.instance.resolveLocalFilesystemUrl(fullPath).then((entry) => <FileEntry> entry);
}
/**
* Calculate the size of a file.
*
* @param path Absolute path to the file.
* @return Promise to be resolved when the size is calculated.
*/
async getExternalFileSize(path: string): Promise<number> {
const fileEntry = await this.getExternalFile(path);
return this.getSize(fileEntry);
}
/**
* Removes a file that might be outside the app's folder.
*
* @param fullPath Absolute path to the file.
* @return Promise to be resolved when the file is removed.
*/
async removeExternalFile(fullPath: string): Promise<void> {
const directory = fullPath.substring(0, fullPath.lastIndexOf('/'));
const filename = fullPath.substr(fullPath.lastIndexOf('/') + 1);
await File.instance.removeFile(directory, filename);
}
/**
* Get the base path where the application files are stored.
*
* @return Promise to be resolved when the base path is retrieved.
*/
getBasePath(): Promise<string> {
return this.init().then(() => {
if (this.basePath.slice(-1) == '/') {
return this.basePath;
} else {
return this.basePath + '/';
}
});
}
/**
* Get the base path where the application files are stored in the format to be used for downloads.
* iOS: Internal URL (cdvfile://).
* Others: basePath (file://)
*
* @return Promise to be resolved when the base path is retrieved.
*/
async getBasePathToDownload(): Promise<string> {
await this.init();
if (CoreApp.instance.isIOS()) {
// In iOS we want the internal URL (cdvfile://localhost/persistent/...).
const dirEntry = await File.instance.resolveDirectoryUrl(this.basePath);
return dirEntry.toInternalURL();
} else {
// In the other platforms we use the basePath as it is (file://...).
return this.basePath;
}
}
/**
* Get the base path where the application files are stored. Returns the value instantly, without waiting for it to be ready.
*
* @return Base path. If the service hasn't been initialized it will return an invalid value.
*/
getBasePathInstant(): string {
if (!this.basePath) {
return this.basePath;
} else if (this.basePath.slice(-1) == '/') {
return this.basePath;
} else {
return this.basePath + '/';
}
}
/**
* Move a dir.
*
* @param originalPath Path to the dir to move.
* @param newPath New path of the dir.
* @param destDirExists Set it to true if you know the directory where to put the dir exists. If false, the function will
* try to create it (slower).
* @return Promise resolved when the entry is moved.
*/
async moveDir(originalPath: string, newPath: string, destDirExists?: boolean): Promise<DirectoryEntry> {
const entry = await this.copyOrMoveFileOrDir(originalPath, newPath, true, false, destDirExists);
return <DirectoryEntry> entry;
}
/**
* Move a file.
*
* @param originalPath Path to the file to move.
* @param newPath New path of the file.
* @param destDirExists Set it to true if you know the directory where to put the file exists. If false, the function will
* try to create it (slower).
* @return Promise resolved when the entry is moved.
*/
async moveFile(originalPath: string, newPath: string, destDirExists?: boolean): Promise<FileEntry> {
const entry = await this.copyOrMoveFileOrDir(originalPath, newPath, false, false, destDirExists);
return <FileEntry> entry;
}
/**
* Copy a directory.
*
* @param from Path to the directory to move.
* @param to New path of the directory.
* @param destDirExists Set it to true if you know the directory where to put the dir exists. If false, the function will
* try to create it (slower).
* @return Promise resolved when the entry is copied.
*/
async copyDir(from: string, to: string, destDirExists?: boolean): Promise<DirectoryEntry> {
const entry = await this.copyOrMoveFileOrDir(from, to, true, true, destDirExists);
return <DirectoryEntry> entry;
}
/**
* Copy a file.
*
* @param from Path to the file to move.
* @param to New path of the file.
* @param destDirExists Set it to true if you know the directory where to put the file exists. If false, the function will
* try to create it (slower).
* @return Promise resolved when the entry is copied.
*/
async copyFile(from: string, to: string, destDirExists?: boolean): Promise<FileEntry> {
const entry = await this.copyOrMoveFileOrDir(from, to, false, true, destDirExists);
return <FileEntry> entry;
}
/**
* Copy or move a file or a directory.
*
* @param from Path to the file/dir to move.
* @param to New path of the file/dir.
* @param isDir Whether it's a dir or a file.
* @param copy Whether to copy. If false, it will move the file.
* @param destDirExists Set it to true if you know the directory where to put the file/dir exists. If false, the function will
* try to create it (slower).
* @return Promise resolved when the entry is copied.
*/
protected async copyOrMoveFileOrDir(
from: string,
to: string,
isDir?: boolean,
copy?: boolean,
destDirExists?: boolean,
): Promise<FileEntry | DirectoryEntry> {
const fileIsInAppFolder = this.isPathInAppFolder(from);
if (!fileIsInAppFolder) {
return this.copyOrMoveExternalFile(from, to, copy);
}
const moveCopyFn: MoveCopyFunction = copy ?
(isDir ? File.instance.copyDir.bind(File.instance) : File.instance.copyFile.bind(File.instance)) :
(isDir ? File.instance.moveDir.bind(File.instance) : File.instance.moveFile.bind(File.instance));
await this.init();
// Paths cannot start with "/". Remove basePath if present.
from = this.removeStartingSlash(from.replace(this.basePath, ''));
to = this.removeStartingSlash(to.replace(this.basePath, ''));
const toFileAndDir = this.getFileAndDirectoryFromPath(to);
if (toFileAndDir.directory && !destDirExists) {
// Create the target directory if it doesn't exist.
await this.createDir(toFileAndDir.directory);
}
try {
const entry = await moveCopyFn(this.basePath, from, this.basePath, to);
return entry;
} catch (error) {
// The copy can fail if the path has encoded characters. Try again if that's the case.
const decodedFrom = decodeURI(from);
const decodedTo = decodeURI(to);
if (from != decodedFrom || to != decodedTo) {
return moveCopyFn(this.basePath, decodedFrom, this.basePath, decodedTo);
} else {
return Promise.reject(error);
}
}
}
/**
* Extract the file name and directory from a given path.
*
* @param path Path to be extracted.
* @return Plain object containing the file name and directory.
* @description
* file.pdf -> directory: '', name: 'file.pdf'
* /file.pdf -> directory: '', name: 'file.pdf'
* path/file.pdf -> directory: 'path', name: 'file.pdf'
* path/ -> directory: 'path', name: ''
* path -> directory: '', name: 'path'
*/
getFileAndDirectoryFromPath(path: string): {directory: string; name: string} {
const file = {
directory: '',
name: '',
};
file.directory = path.substring(0, path.lastIndexOf('/'));
file.name = path.substr(path.lastIndexOf('/') + 1);
return file;
}
/**
* Get the internal URL of a file.
* Please notice that with WKWebView these URLs no longer work in mobile. Use fileEntry.toURL() along with convertFileSrc.
*
* @param fileEntry File Entry.
* @return Internal URL.
*/
getInternalURL(fileEntry: FileEntry): string {
if (!fileEntry.toInternalURL) {
// File doesn't implement toInternalURL, use toURL.
return fileEntry.toURL();
}
return fileEntry.toInternalURL();
}
/**
* Adds the basePath to a path if it doesn't have it already.
*
* @param path Path to treat.
* @return Path with basePath added.
*/
addBasePathIfNeeded(path: string): string {
if (path.indexOf(this.basePath) > -1) {
return path;
} else {
return CoreTextUtils.instance.concatenatePaths(this.basePath, path);
}
}
/**
* Remove the base path from a path. If basePath isn't found, return false.
*
* @param path Path to treat.
* @return Path without basePath if basePath was found, undefined otherwise.
*/
removeBasePath(path: string): string {
if (path.indexOf(this.basePath) > -1) {
return path.replace(this.basePath, '');
}
return path;
}
/**
* Unzips a file.
*
* @param path Path to the ZIP file.
* @param destFolder Path to the destination folder. If not defined, a new folder will be created with the
* same location and name as the ZIP file (without extension).
* @param onProgress Function to call on progress.
* @param recreateDir Delete the dest directory before unzipping. Defaults to true.
* @return Promise resolved when the file is unzipped.
*/
async unzipFile(
path: string,
destFolder?: string,
onProgress?: (progress: ProgressEvent) => void,
recreateDir: boolean = true,
): Promise<void> {
// Get the source file.
const fileEntry = await this.getFile(path);
if (destFolder && recreateDir) {
// Make sure the dest dir doesn't exist already.
await CoreUtils.instance.ignoreErrors(this.removeDir(destFolder));
// Now create the dir, otherwise if any of the ancestor dirs doesn't exist the unzip would fail.
await this.createDir(destFolder);
}
// If destFolder is not set, use same location as ZIP file. We need to use absolute paths (including basePath).
destFolder = this.addBasePathIfNeeded(destFolder || CoreMimetypeUtils.instance.removeExtension(path));
const result = await Zip.instance.unzip(fileEntry.toURL(), destFolder, onProgress);
if (result == -1) {
throw new CoreError('Unzip failed.');
}
}
/**
* Search a string or regexp in a file contents and replace it. The result is saved in the same file.
*
* @param path Path to the file.
* @param search Value to search.
* @param newValue New value.
* @return Promise resolved in success.
*/
async replaceInFile(path: string, search: string | RegExp, newValue: string): Promise<void> {
let content = <string> await this.readFile(path);
if (typeof content == 'undefined' || content === null || !content.replace) {
throw new CoreError(`Error reading file ${path}`);
}
if (content.match(search)) {
content = content.replace(search, newValue);
await this.writeFile(path, content);
}
}
/**
* Get a file/dir metadata given the file's entry.
*
* @param fileEntry FileEntry retrieved from getFile or similar.
* @return Promise resolved with metadata.
*/
getMetadata(fileEntry: Entry): Promise<Metadata> {
if (!fileEntry || !fileEntry.getMetadata) {
return Promise.reject(new CoreError('Cannot get metadata from file entry.'));
}
return new Promise((resolve, reject): void => {
fileEntry.getMetadata(resolve, reject);
});
}
/**
* Get a file/dir metadata given the path.
*
* @param path Path to the file/dir.
* @param isDir True if directory, false if file.
* @return Promise resolved with metadata.
*/
getMetadataFromPath(path: string, isDir?: boolean): Promise<Metadata> {
let promise;
if (isDir) {
promise = this.getDir(path);
} else {
promise = this.getFile(path);
}
return promise.then((entry) => this.getMetadata(entry));
}
/**
* Remove the starting slash of a path if it's there. E.g. '/sites/filepool' -> 'sites/filepool'.
*
* @param path Path.
* @return Path without a slash in the first position.
*/
removeStartingSlash(path: string): string {
if (path[0] == '/') {
return path.substr(1);
}
return path;
}
/**
* Convenience function to copy or move an external file.
*
* @param from Absolute path to the file to copy/move.
* @param to Relative new path of the file (inside the app folder).
* @param copy True to copy, false to move.
* @return Promise resolved when the entry is copied/moved.
*/
protected async copyOrMoveExternalFile(from: string, to: string, copy?: boolean): Promise<FileEntry> {
// Get the file to copy/move.
const fileEntry = await this.getExternalFile(from);
// Create the destination dir if it doesn't exist.
const dirAndFile = this.getFileAndDirectoryFromPath(to);
const dirEntry = await this.createDir(dirAndFile.directory);
// Now copy/move the file.
return new Promise((resolve, reject): void => {
if (copy) {
fileEntry.copyTo(dirEntry, dirAndFile.name, (entry: FileEntry) => resolve(entry), reject);
} else {
fileEntry.moveTo(dirEntry, dirAndFile.name, (entry: FileEntry) => resolve(entry), reject);
}
});
}
/**
* Copy a file from outside of the app folder to somewhere inside the app folder.
*
* @param from Absolute path to the file to copy.
* @param to Relative new path of the file (inside the app folder).
* @return Promise resolved when the entry is copied.
*/
copyExternalFile(from: string, to: string): Promise<FileEntry> {
return this.copyOrMoveExternalFile(from, to, true);
}
/**
* Move a file from outside of the app folder to somewhere inside the app folder.
*
* @param from Absolute path to the file to move.
* @param to Relative new path of the file (inside the app folder).
* @return Promise resolved when the entry is moved.
*/
moveExternalFile(from: string, to: string): Promise<FileEntry> {
return this.copyOrMoveExternalFile(from, to, false);
}
/**
* Get a unique file name inside a folder, adding numbers to the file name if needed.
*
* @param dirPath Path to the destination folder.
* @param fileName File name that wants to be used.
* @param defaultExt Default extension to use if no extension found in the file.
* @return Promise resolved with the unique file name.
*/
async getUniqueNameInFolder(dirPath: string, fileName: string, defaultExt?: string): Promise<string> {
// Get existing files in the folder.
try {
const entries = await this.getDirectoryContents(dirPath);
const files = {};
let num = 1;
let fileNameWithoutExtension = CoreMimetypeUtils.instance.removeExtension(fileName);
let extension = CoreMimetypeUtils.instance.getFileExtension(fileName) || defaultExt;
// Clean the file name.
fileNameWithoutExtension = CoreTextUtils.instance.removeSpecialCharactersForFiles(
CoreTextUtils.instance.decodeURIComponent(fileNameWithoutExtension),
);
// Index the files by name.
entries.forEach((entry) => {
files[entry.name.toLowerCase()] = entry;
});
// Format extension.
if (extension) {
extension = '.' + extension;
} else {
extension = '';
}
let newName = fileNameWithoutExtension + extension;
if (typeof files[newName.toLowerCase()] == 'undefined') {
// No file with the same name.
return newName;
} else {
// Repeated name. Add a number until we find a free name.
do {
newName = fileNameWithoutExtension + '(' + num + ')' + extension;
num++;
} while (typeof files[newName.toLowerCase()] != 'undefined');
// Ask the user what he wants to do.
return newName;
}
} catch (error) {
// Folder doesn't exist, name is unique. Clean it and return it.
return CoreTextUtils.instance.removeSpecialCharactersForFiles(CoreTextUtils.instance.decodeURIComponent(fileName));
}
}
/**
* Remove app temporary folder.
*
* @return Promise resolved when done.
*/
async clearTmpFolder(): Promise<void> {
// Ignore errors because the folder might not exist.
await CoreUtils.instance.ignoreErrors(this.removeDir(CoreFileProvider.TMPFOLDER));
}
/**
* Given a folder path and a list of used files, remove all the files of the folder that aren't on the list of used files.
*
* @param dirPath Folder path.
* @param files List of used files.
* @return Promise resolved when done, rejected if failure.
*/
async removeUnusedFiles(dirPath: string, files: (CoreWSExternalFile | FileEntry)[]): Promise<void> {
// Get the directory contents.
try {
const contents = await this.getDirectoryContents(dirPath);
if (!contents.length) {
return;
}
const filesMap: {[fullPath: string]: FileEntry} = {};
const promises: Promise<void>[] = [];
// Index the received files by fullPath and ignore the invalid ones.
files.forEach((file) => {
if ('fullPath' in file) {
filesMap[file.fullPath] = file;
}
});
// Check which of the content files aren't used anymore and delete them.
contents.forEach((file) => {
if (!filesMap[file.fullPath]) {
// File isn't used, delete it.
promises.push(this.removeFileByFileEntry(file));
}
});
await Promise.all(promises);
} catch (error) {
// Ignore errors, maybe it doesn't exist.
}
}
/**
* Check if a file is inside the app's folder.
*
* @param path The absolute path of the file to check.
* @return Whether the file is in the app's folder.
*/
isFileInAppFolder(path: string): boolean {
return path.indexOf(this.basePath) != -1;
}
/**
* Get the path to the www folder at runtime based on the WebView URL.
*
* @return Path.
*/
getWWWPath(): string {
const position = window.location.href.indexOf('index.html');
if (position != -1) {
return window.location.href.substr(0, position);
}
return window.location.href;
}
/**
* Get the full path to the www folder.
*
* @return Path.
*/
getWWWAbsolutePath(): string {
if (cordova && cordova.file && cordova.file.applicationDirectory) {
return CoreTextUtils.instance.concatenatePaths(cordova.file.applicationDirectory, 'www');
}
// Cannot use Cordova to get it, use the WebView URL.
return this.getWWWPath();
}
/**
* Helper function to call Ionic WebView convertFileSrc only in the needed platforms.
* This is needed to make files work with the Ionic WebView plugin.
*
* @param src Source to convert.
* @return Converted src.
*/
convertFileSrc(src: string): string {
return CoreApp.instance.isIOS() ? WebView.instance.convertFileSrc(src) : src;
}
/**
* Undo the conversion of convertFileSrc.
*
* @param src Source to unconvert.
* @return Unconverted src.
*/
unconvertFileSrc(src: string): string {
if (!CoreApp.instance.isIOS()) {
return src;
}
return src.replace(CoreConstants.CONFIG.ioswebviewscheme + '://localhost/_app_file_', 'file://');
}
/**
* Check if a certain path is in the app's folder (basePath).
*
* @param path Path to check.
* @return Whether it's in the app folder.
*/
protected isPathInAppFolder(path: string): boolean {
return !path || !path.match(/^[a-z0-9]+:\/\//i) || path.indexOf(this.basePath) != -1;
}
}
export class CoreFile extends makeSingleton(CoreFileProvider) {}
type MoveCopyFunction = (path: string, dirName: string, newPath: string, newDirName: string) => Promise<FileEntry | DirectoryEntry>;