Vmeda.Online/src/core/h5p/classes/file-storage.ts

458 lines
16 KiB
TypeScript

// (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 { CoreFile } from '@providers/file';
import { CoreFilepool } from '@providers/filepool';
import { CoreSites } from '@providers/sites';
import { CoreMimetypeUtils } from '@providers/utils/mimetype';
import { CoreTextUtils } from '@providers/utils/text';
import { CoreUtils } from '@providers/utils/utils';
import { CoreH5PProvider } from '../providers/h5p';
import { CoreH5PCore, CoreH5PDependencyAsset, CoreH5PContentDependencyData, CoreH5PDependenciesFiles } from './core';
import { CoreH5PLibrariesCachedAssetsDBData } from './framework';
/**
* Equivalent to Moodle's implementation of H5PFileStorage.
*/
export class CoreH5PFileStorage {
static CACHED_ASSETS_FOLDER_NAME = 'cachedassets';
/**
* Will concatenate all JavaScrips and Stylesheets into two files in order to improve page performance.
*
* @param files A set of all the assets required for content to display.
* @param key Hashed key for cached asset.
* @param folderName Name of the folder of the H5P package.
* @param siteId The site ID.
* @return Promise resolved when done.
*/
async cacheAssets(files: CoreH5PDependenciesFiles, key: string, folderName: string, siteId: string): Promise<void> {
const cachedAssetsPath = this.getCachedAssetsFolderPath(folderName, siteId);
// Treat each type in the assets.
await Promise.all(Object.keys(files).map(async (type) => {
const assets: CoreH5PDependencyAsset[] = files[type];
if (!assets || !assets.length) {
return;
}
// Create new file for cached assets.
const fileName = key + '.' + (type == 'scripts' ? 'js' : 'css');
const path = CoreTextUtils.instance.concatenatePaths(cachedAssetsPath, fileName);
// Store concatenated content.
const content = await this.concatenateFiles(assets, type);
await CoreFile.instance.writeFile(path, content);
// Now update the files data.
files[type] = [
{
path: CoreTextUtils.instance.concatenatePaths(CoreH5PFileStorage.CACHED_ASSETS_FOLDER_NAME, fileName),
version: ''
}
];
}));
}
/**
* Adds all files of a type into one file.
*
* @param assets A list of files.
* @param type The type of files in assets. Either 'scripts' or 'styles'
* @return Promise resolved with all of the files content in one string.
*/
protected async concatenateFiles(assets: CoreH5PDependencyAsset[], type: string): Promise<string> {
const basePath = CoreFile.instance.convertFileSrc(CoreFile.instance.getBasePathInstant());
let content = '';
for (const i in assets) {
const asset = assets[i];
let fileContent: string = await CoreFile.instance.readFile(asset.path);
if (type == 'scripts') {
// No need to treat scripts, just append the content.
content += fileContent + ';\n';
} else {
// Rewrite relative URLs used inside stylesheets.
const matches = fileContent.match(/url\([\'"]?([^"\')]+)[\'"]?\)/ig);
const assetPath = asset.path.replace(/(^\/|\/$)/g, ''); // Path without start/end slashes.
const treated = {};
if (matches && matches.length) {
matches.forEach((match) => {
let url = match.replace(/(url\(['"]?|['"]?\)$)/ig, '');
if (treated[url] || url.match(/^(data:|([a-z0-9]+:)?\/)/i)) {
return; // Not relative or already treated, skip.
}
const pathSplit = assetPath.split('/');
treated[url] = url;
/* Find "../" in the URL. If it exists, we have to remove "../" and switch the last folder in the
filepath for the first folder in the url. */
if (url.match(/^\.\.\//)) {
const urlSplit = url.split('/').filter((i) => {
return i; // Remove empty values.
});
// Remove the file name from the asset path.
pathSplit.pop();
// Remove the first element from the file URL: ../ .
urlSplit.shift();
// Put the url's first folder into the asset path.
pathSplit[pathSplit.length - 1] = urlSplit[0];
urlSplit.shift();
// Create the new URL and replace it in the file contents.
url = pathSplit.join('/') + '/' + urlSplit.join('/');
} else {
pathSplit[pathSplit.length - 1] = url; // Put the whole path to the end of the asset path.
url = pathSplit.join('/');
}
fileContent = fileContent.replace(new RegExp(CoreTextUtils.instance.escapeForRegex(match), 'g'),
'url("' + CoreTextUtils.instance.concatenatePaths(basePath, url) + '")');
});
}
content += fileContent + '\n';
}
}
return content;
}
/**
* Delete cached assets from file system.
*
* @param libraryId Library identifier.
* @param siteId Site ID. If not defined, current site.
* @return Promise resolved when done.
*/
async deleteCachedAssets(removedEntries: CoreH5PLibrariesCachedAssetsDBData[], siteId?: string): Promise<void> {
const site = await CoreSites.instance.getSite(siteId);
const promises = [];
removedEntries.forEach((entry) => {
const cachedAssetsFolder = this.getCachedAssetsFolderPath(entry.foldername, site.getId());
['js', 'css'].forEach((type) => {
const path = CoreTextUtils.instance.concatenatePaths(cachedAssetsFolder, entry.hash + '.' + type);
promises.push(CoreFile.instance.removeFile(path));
});
});
try {
await CoreUtils.instance.allPromises(promises);
} catch (error) {
// Ignore errors, maybe there's no cached asset of some type.
}
}
/**
* Deletes a content folder from the file system.
*
* @param folderName Folder name of the content.
* @param siteId Site ID. If not defined, current site.
* @return Promise resolved when done.
*/
async deleteContentFolder(folderName: string, siteId?: string): Promise<void> {
await CoreFile.instance.removeDir(this.getContentFolderPath(folderName, siteId));
}
/**
* Delete content indexes from filesystem.
*
* @param folderName Name of the folder of the H5P package.
* @param siteId Site ID. If not defined, current site.
* @return Promise resolved when done.
*/
async deleteContentIndex(folderName: string, siteId?: string): Promise<void> {
await CoreFile.instance.removeFile(this.getContentIndexPath(folderName, siteId));
}
/**
* Delete content indexes from filesystem.
*
* @param libraryId Library identifier.
* @param siteId Site ID. If not defined, current site.
* @return Promise resolved when done.
*/
async deleteContentIndexesForLibrary(libraryId: number, siteId?: string): Promise<void> {
const site = await CoreSites.instance.getSite(siteId);
const db = site.getDb();
// Get the folder names of all the packages that use this library.
const query = 'SELECT DISTINCT hc.foldername ' +
'FROM ' + CoreH5PProvider.CONTENTS_LIBRARIES_TABLE + ' hcl ' +
'JOIN ' + CoreH5PProvider.CONTENT_TABLE + ' hc ON hcl.h5pid = hc.id ' +
'WHERE hcl.libraryid = ?';
const queryArgs = [];
queryArgs.push(libraryId);
const result = await db.execute(query, queryArgs);
await Array.from(result.rows).map(async (entry: {foldername: string}) => {
try {
// Delete the index.html.
await this.deleteContentIndex(entry.foldername, site.getId());
} catch (error) {
// Ignore errors.
}
});
}
/**
* Deletes a library from the file system.
*
* @param libraryData The library data.
* @param folderName Folder name. If not provided, it will be calculated.
* @param siteId Site ID. If not defined, current site.
* @return Promise resolved when done.
*/
async deleteLibraryFolder(libraryData: any, folderName?: string, siteId?: string): Promise<void> {
await CoreFile.instance.removeDir(this.getLibraryFolderPath(libraryData, siteId, folderName));
}
/**
* Will check if there are cache assets available for content.
*
* @param key Hashed key for cached asset
* @return Promise resolved with the files.
*/
async getCachedAssets(key: string): Promise<{scripts?: CoreH5PDependencyAsset[], styles?: CoreH5PDependencyAsset[]}> {
// Get JS and CSS cached assets if they exist.
const results = await Promise.all([
this.getCachedAsset(key, '.js'),
this.getCachedAsset(key, '.css'),
]);
const files = {
scripts: results[0],
styles: results[1],
};
return files.scripts || files.styles ? files : null;
}
/**
* Check if a cached asset file exists and, if so, return its data.
*
* @param key Key of the cached asset.
* @param extension Extension of the file to get.
* @return Promise resolved with the list of assets (only one), undefined if not found.
*/
protected async getCachedAsset(key: string, extension: string): Promise<CoreH5PDependencyAsset[]> {
try {
const path = CoreTextUtils.instance.concatenatePaths(CoreH5PFileStorage.CACHED_ASSETS_FOLDER_NAME, key + extension);
const size = await CoreFile.instance.getFileSize(path);
if (size > 0) {
return [
{
path: path,
version: '',
},
];
}
} catch (error) {
// Not found, nothing to do.
}
}
/**
* Get relative path to a content cached assets.
*
* @param folderName Name of the folder of the content the assets belong to.
* @param siteId Site ID.
* @return Path.
*/
getCachedAssetsFolderPath(folderName: string, siteId: string): string {
return CoreTextUtils.instance.concatenatePaths(
this.getContentFolderPath(folderName, siteId), CoreH5PFileStorage.CACHED_ASSETS_FOLDER_NAME);
}
/**
* Get a content folder name given the package URL.
*
* @param fileUrl Package URL.
* @param siteId Site ID.
* @return Promise resolved with the folder name.
*/
async getContentFolderNameByUrl(fileUrl: string, siteId: string): Promise<string> {
const path = await CoreFilepool.instance.getFilePathByUrl(siteId, fileUrl);
const fileAndDir = CoreFile.instance.getFileAndDirectoryFromPath(path);
return CoreMimetypeUtils.instance.removeExtension(fileAndDir.name);
}
/**
* Get a package content path.
*
* @param folderName Name of the folder of the H5P package.
* @param siteId The site ID.
* @return Folder path.
*/
getContentFolderPath(folderName: string, siteId: string): string {
return CoreTextUtils.instance.concatenatePaths(
this.getExternalH5PFolderPath(siteId), 'packages/' + folderName + '/content');
}
/**
* Get the content index file.
*
* @param fileUrl URL of the H5P package.
* @param siteId The site ID. If not defined, current site.
* @return Promise resolved with the file URL if exists, rejected otherwise.
*/
async getContentIndexFileUrl(fileUrl: string, siteId?: string): Promise<string> {
siteId = siteId || CoreSites.instance.getCurrentSiteId();
const folderName = await this.getContentFolderNameByUrl(fileUrl, siteId);
const file = await CoreFile.instance.getFile(this.getContentIndexPath(folderName, siteId));
return file.toURL();
}
/**
* Get the path to a content index.
*
* @param folderName Name of the folder of the H5P package.
* @param siteId The site ID.
* @return Folder path.
*/
getContentIndexPath(folderName: string, siteId: string): string {
return CoreTextUtils.instance.concatenatePaths(this.getContentFolderPath(folderName, siteId), 'index.html');
}
/**
* Get the path to the folder that contains the H5P core libraries.
*
* @return Folder path.
*/
getCoreH5PPath(): string {
return CoreTextUtils.instance.concatenatePaths(CoreFile.instance.getWWWPath(), '/h5p/');
}
/**
* Get the path to the dependency.
*
* @param dependency Dependency library.
* @return The path to the dependency library
*/
getDependencyPath(dependency: CoreH5PContentDependencyData): string {
return 'libraries/' + dependency.machineName + '-' + dependency.majorVersion + '.' + dependency.minorVersion;
}
/**
* Get path to the folder containing H5P files extracted from packages.
*
* @param siteId The site ID.
* @return Folder path.
*/
getExternalH5PFolderPath(siteId: string): string {
return CoreTextUtils.instance.concatenatePaths(CoreFile.instance.getSiteFolder(siteId), 'h5p');
}
/**
* Get libraries folder path.
*
* @param siteId The site ID.
* @return Folder path.
*/
getLibrariesFolderPath(siteId: string): string {
return CoreTextUtils.instance.concatenatePaths(this.getExternalH5PFolderPath(siteId), 'libraries');
}
/**
* Get a library's folder path.
*
* @param libraryData The library data.
* @param siteId The site ID.
* @param folderName Folder name. If not provided, it will be calculated.
* @return Folder path.
*/
getLibraryFolderPath(libraryData: any, siteId: string, folderName?: string): string {
if (!folderName) {
folderName = CoreH5PCore.libraryToString(libraryData, true);
}
return CoreTextUtils.instance.concatenatePaths(this.getLibrariesFolderPath(siteId), folderName);
}
/**
* Save the content in filesystem.
*
* @param contentPath Path to the current content folder (tmp).
* @param folderName Name to put to the content folder.
* @param siteId Site ID.
* @return Promise resolved when done.
*/
async saveContent(contentPath: string, folderName: string, siteId: string): Promise<void> {
const folderPath = this.getContentFolderPath(folderName, siteId);
// Delete existing content for this package.
try {
await CoreFile.instance.removeDir(folderPath);
} catch (error) {
// Ignore errors, maybe it doesn't exist.
}
// Copy the new one.
await CoreFile.instance.moveDir(contentPath, folderPath);
}
/**
* Save a library in filesystem.
*
* @param libraryData Library data.
* @param siteId Site ID. If not defined, current site.
* @return Promise resolved when done.
*/
async saveLibrary(libraryData: any, siteId?: string): Promise<void> {
const folderPath = this.getLibraryFolderPath(libraryData, siteId);
// Delete existing library version.
try {
await CoreFile.instance.removeDir(folderPath);
} catch (error) {
// Ignore errors, maybe it doesn't exist.
}
// Copy the new one.
await CoreFile.instance.moveDir(libraryData.uploadDirectory, folderPath, true);
}
}