458 lines
16 KiB
TypeScript
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);
|
|
}
|
|
}
|