2020-10-07 10:53:19 +02:00
|
|
|
// (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 { Md5 } from 'ts-md5/dist/md5';
|
|
|
|
|
2020-10-28 14:25:18 +01:00
|
|
|
import { CoreApp } from '@services/app';
|
2020-12-11 15:40:34 +01:00
|
|
|
import { CoreEventPackageStatusChanged, CoreEvents } from '@singletons/events';
|
2020-10-07 10:53:19 +02:00
|
|
|
import { CoreFile } from '@services/file';
|
2020-12-09 16:42:21 +01:00
|
|
|
import { CorePluginFileDelegate } from '@services/plugin-file-delegate';
|
2020-10-28 14:25:18 +01:00
|
|
|
import { CoreSites } from '@services/sites';
|
2020-10-07 10:53:19 +02:00
|
|
|
import { CoreWS, CoreWSExternalFile } from '@services/ws';
|
|
|
|
import { CoreDomUtils } from '@services/utils/dom';
|
|
|
|
import { CoreMimetypeUtils } from '@services/utils/mimetype';
|
|
|
|
import { CoreTextUtils } from '@services/utils/text';
|
|
|
|
import { CoreTimeUtils } from '@services/utils/time';
|
|
|
|
import { CoreUrlUtils } from '@services/utils/url';
|
2020-10-08 16:33:10 +02:00
|
|
|
import { CoreUtils, PromiseDefer } from '@services/utils/utils';
|
2020-10-07 10:53:19 +02:00
|
|
|
import { SQLiteDB } from '@classes/sqlitedb';
|
2020-10-14 16:38:24 +02:00
|
|
|
import { CoreError } from '@classes/errors/error';
|
2020-11-19 12:40:18 +01:00
|
|
|
import { CoreConstants } from '@/core/constants';
|
2020-12-01 18:35:07 +01:00
|
|
|
import { ApplicationInit, makeSingleton, Network, NgZone, Translate } from '@singletons';
|
2020-10-07 10:53:19 +02:00
|
|
|
import { CoreLogger } from '@singletons/logger';
|
2020-10-28 14:25:18 +01:00
|
|
|
import {
|
|
|
|
APP_SCHEMA,
|
|
|
|
FILES_TABLE_NAME,
|
|
|
|
QUEUE_TABLE_NAME,
|
|
|
|
PACKAGES_TABLE_NAME,
|
|
|
|
LINKS_TABLE_NAME,
|
|
|
|
CoreFilepoolFileEntry,
|
|
|
|
CoreFilepoolComponentLink,
|
|
|
|
CoreFilepoolFileOptions,
|
|
|
|
CoreFilepoolLinksRecord,
|
|
|
|
CoreFilepoolPackageEntry,
|
|
|
|
CoreFilepoolQueueEntry,
|
|
|
|
CoreFilepoolQueueDBEntry,
|
2020-12-01 18:37:24 +01:00
|
|
|
} from '@services/database/filepool';
|
2020-10-07 10:53:19 +02:00
|
|
|
|
|
|
|
/*
|
|
|
|
* Factory for handling downloading files and retrieve downloaded files.
|
|
|
|
*
|
|
|
|
* @description
|
|
|
|
* This factory is responsible for handling downloading files.
|
|
|
|
*
|
|
|
|
* The two main goals of this is to keep the content available offline, and improve the user experience by caching
|
|
|
|
* the content locally.
|
|
|
|
*/
|
2020-11-19 16:35:17 +01:00
|
|
|
@Injectable({ providedIn: 'root' })
|
2020-10-07 10:53:19 +02:00
|
|
|
export class CoreFilepoolProvider {
|
2020-10-08 16:33:10 +02:00
|
|
|
|
2020-10-07 10:53:19 +02:00
|
|
|
// Constants.
|
2020-10-08 16:33:10 +02:00
|
|
|
protected static readonly QUEUE_PROCESS_INTERVAL = 0;
|
|
|
|
protected static readonly FOLDER = 'filepool';
|
|
|
|
protected static readonly WIFI_DOWNLOAD_THRESHOLD = 20971520; // 20MB.
|
|
|
|
protected static readonly DOWNLOAD_THRESHOLD = 2097152; // 2MB.
|
|
|
|
protected static readonly QUEUE_RUNNING = 'CoreFilepool:QUEUE_RUNNING';
|
|
|
|
protected static readonly QUEUE_PAUSED = 'CoreFilepool:QUEUE_PAUSED';
|
|
|
|
protected static readonly ERR_QUEUE_IS_EMPTY = 'CoreFilepoolError:ERR_QUEUE_IS_EMPTY';
|
|
|
|
protected static readonly ERR_FS_OR_NETWORK_UNAVAILABLE = 'CoreFilepoolError:ERR_FS_OR_NETWORK_UNAVAILABLE';
|
|
|
|
protected static readonly ERR_QUEUE_ON_PAUSE = 'CoreFilepoolError:ERR_QUEUE_ON_PAUSE';
|
|
|
|
|
2020-10-14 16:38:24 +02:00
|
|
|
protected static readonly FILE_UPDATE_UNKNOWN_WHERE_CLAUSE =
|
2020-10-07 10:53:19 +02:00
|
|
|
'isexternalfile = 1 OR ((revision IS NULL OR revision = 0) AND (timemodified IS NULL OR timemodified = 0))';
|
|
|
|
|
|
|
|
protected logger: CoreLogger;
|
2020-10-21 16:32:27 +02:00
|
|
|
protected queueState = CoreFilepoolProvider.QUEUE_PAUSED;
|
2020-10-14 16:38:24 +02:00
|
|
|
protected urlAttributes: RegExp[] = [
|
2020-10-08 16:33:10 +02:00
|
|
|
new RegExp('(\\?|&)token=([A-Za-z0-9]*)'),
|
2020-10-07 10:53:19 +02:00
|
|
|
new RegExp('(\\?|&)forcedownload=[0-1]'),
|
|
|
|
new RegExp('(\\?|&)preview=[A-Za-z0-9]+'),
|
2020-10-08 16:33:10 +02:00
|
|
|
new RegExp('(\\?|&)offline=[0-1]', 'g'),
|
2020-10-07 10:53:19 +02:00
|
|
|
];
|
2020-10-08 16:33:10 +02:00
|
|
|
|
|
|
|
// To handle file downloads using the queue.
|
|
|
|
protected queueDeferreds: { [s: string]: { [s: string]: CoreFilepoolPromiseDefer } } = {};
|
2020-10-14 16:38:24 +02:00
|
|
|
protected sizeCache: {[fileUrl: string]: number} = {}; // A "cache" to store file sizes.
|
2020-10-07 10:53:19 +02:00
|
|
|
// Variables to prevent downloading packages/files twice at the same time.
|
2020-10-08 16:33:10 +02:00
|
|
|
protected packagesPromises: { [s: string]: { [s: string]: Promise<void> } } = {};
|
|
|
|
protected filePromises: { [s: string]: { [s: string]: Promise<string> } } = {};
|
2020-10-07 10:53:19 +02:00
|
|
|
|
2020-12-01 18:37:24 +01:00
|
|
|
// Variables for DB.
|
|
|
|
protected appDB: Promise<SQLiteDB>;
|
|
|
|
protected resolveAppDB!: (appDB: SQLiteDB) => void;
|
|
|
|
|
2020-10-07 10:53:19 +02:00
|
|
|
constructor() {
|
2020-12-01 18:37:24 +01:00
|
|
|
this.appDB = new Promise(resolve => this.resolveAppDB = resolve);
|
2020-10-07 10:53:19 +02:00
|
|
|
this.logger = CoreLogger.getInstance('CoreFilepoolProvider');
|
|
|
|
|
2020-12-01 18:37:24 +01:00
|
|
|
this.init();
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
2020-12-09 13:39:18 +01:00
|
|
|
* Initialize database.
|
2020-12-01 18:37:24 +01:00
|
|
|
*/
|
2020-12-09 13:39:18 +01:00
|
|
|
async initializeDatabase(): Promise<void> {
|
2020-12-01 18:37:24 +01:00
|
|
|
try {
|
2021-03-02 11:41:04 +01:00
|
|
|
await CoreApp.createTablesFromSchema(APP_SCHEMA);
|
2020-12-01 18:37:24 +01:00
|
|
|
} catch (e) {
|
2020-10-07 10:53:19 +02:00
|
|
|
// Ignore errors.
|
2020-12-01 18:37:24 +01:00
|
|
|
}
|
2020-10-07 10:53:19 +02:00
|
|
|
|
2021-03-02 11:41:04 +01:00
|
|
|
this.resolveAppDB(CoreApp.getDB());
|
2020-10-21 16:32:27 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Init some properties.
|
|
|
|
*/
|
|
|
|
protected async init(): Promise<void> {
|
|
|
|
// Waiting for the app to be ready to start processing the queue.
|
2021-03-02 11:41:04 +01:00
|
|
|
await ApplicationInit.donePromise;
|
2020-10-21 16:32:27 +02:00
|
|
|
|
|
|
|
this.checkQueueProcessing();
|
2020-10-07 10:53:19 +02:00
|
|
|
|
2020-10-21 16:32:27 +02:00
|
|
|
// Start queue when device goes online.
|
2021-03-02 11:41:04 +01:00
|
|
|
Network.onConnect().subscribe(() => {
|
2020-10-21 16:32:27 +02:00
|
|
|
// Execute the callback in the Angular zone, so change detection doesn't stop working.
|
2021-03-02 11:41:04 +01:00
|
|
|
NgZone.run(() => {
|
2020-10-21 16:32:27 +02:00
|
|
|
this.checkQueueProcessing();
|
2020-10-07 10:53:19 +02:00
|
|
|
});
|
|
|
|
});
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Link a file with a component.
|
|
|
|
*
|
|
|
|
* @param siteId The site ID.
|
|
|
|
* @param fileId The file ID.
|
|
|
|
* @param component The component to link the file to.
|
|
|
|
* @param componentId An ID to use in conjunction with the component.
|
|
|
|
* @return Promise resolved on success.
|
|
|
|
*/
|
2020-10-08 16:33:10 +02:00
|
|
|
protected async addFileLink(siteId: string, fileId: string, component: string, componentId?: string | number): Promise<void> {
|
2020-10-07 10:53:19 +02:00
|
|
|
if (!component) {
|
2020-10-14 16:38:24 +02:00
|
|
|
throw new CoreError('Cannot add link because component is invalid.');
|
2020-10-07 10:53:19 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
componentId = this.fixComponentId(componentId);
|
|
|
|
|
2021-03-02 11:41:04 +01:00
|
|
|
const db = await CoreSites.getSiteDb(siteId);
|
2020-10-08 16:33:10 +02:00
|
|
|
const newEntry: CoreFilepoolLinksRecord = {
|
|
|
|
fileId,
|
|
|
|
component,
|
|
|
|
componentId: componentId || '',
|
|
|
|
};
|
2020-10-07 10:53:19 +02:00
|
|
|
|
2020-10-28 14:25:18 +01:00
|
|
|
await db.insertRecord(LINKS_TABLE_NAME, newEntry);
|
2020-10-07 10:53:19 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Link a file with a component by URL.
|
|
|
|
*
|
|
|
|
* @param siteId The site ID.
|
|
|
|
* @param fileUrl The file Url.
|
|
|
|
* @param component The component to link the file to.
|
|
|
|
* @param componentId An ID to use in conjunction with the component.
|
|
|
|
* @return Promise resolved on success.
|
|
|
|
* @description
|
|
|
|
* Use this method to create a link between a URL and a component. You usually do not need to call this manually since
|
|
|
|
* downloading a file automatically does this. Note that this method does not check if the file exists in the pool.
|
|
|
|
*/
|
2020-10-08 16:33:10 +02:00
|
|
|
async addFileLinkByUrl(siteId: string, fileUrl: string, component: string, componentId?: string | number): Promise<void> {
|
|
|
|
const file = await this.fixPluginfileURL(siteId, fileUrl);
|
|
|
|
const fileId = this.getFileIdByUrl(file.fileurl);
|
2020-10-07 10:53:19 +02:00
|
|
|
|
2020-10-08 16:33:10 +02:00
|
|
|
await this.addFileLink(siteId, fileId, component, componentId);
|
2020-10-07 10:53:19 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Link a file with several components.
|
|
|
|
*
|
|
|
|
* @param siteId The site ID.
|
|
|
|
* @param fileId The file ID.
|
|
|
|
* @param links Array of objects containing the component and optionally componentId.
|
|
|
|
* @return Promise resolved on success.
|
|
|
|
*/
|
2020-10-08 16:33:10 +02:00
|
|
|
protected async addFileLinks(siteId: string, fileId: string, links: CoreFilepoolComponentLink[]): Promise<void> {
|
|
|
|
const promises = links.map((link) => this.addFileLink(siteId, fileId, link.component, link.componentId));
|
2020-10-07 10:53:19 +02:00
|
|
|
|
2020-10-08 16:33:10 +02:00
|
|
|
await Promise.all(promises);
|
2020-10-07 10:53:19 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Add files to queue using a URL.
|
|
|
|
*
|
|
|
|
* @param siteId The site ID.
|
|
|
|
* @param files Array of files to add.
|
|
|
|
* @param component The component to link the file to.
|
|
|
|
* @param componentId An ID to use in conjunction with the component (optional).
|
|
|
|
* @return Resolved on success.
|
|
|
|
*/
|
2020-10-08 16:33:10 +02:00
|
|
|
addFilesToQueue(siteId: string, files: CoreWSExternalFile[], component?: string, componentId?: string | number): Promise<void> {
|
2020-10-07 10:53:19 +02:00
|
|
|
return this.downloadOrPrefetchFiles(siteId, files, true, false, component, componentId);
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Add a file to the pool.
|
|
|
|
*
|
|
|
|
* @param siteId The site ID.
|
|
|
|
* @param fileId The file ID.
|
|
|
|
* @param data Additional information to store about the file (timemodified, url, ...). See FILES_TABLE schema.
|
|
|
|
* @return Promise resolved on success.
|
|
|
|
*/
|
2020-10-21 16:32:27 +02:00
|
|
|
protected async addFileToPool(siteId: string, fileId: string, data: Omit<CoreFilepoolFileEntry, 'fileId'>): Promise<void> {
|
2020-10-14 16:38:24 +02:00
|
|
|
const record = {
|
|
|
|
fileId,
|
|
|
|
...data,
|
|
|
|
};
|
2020-10-07 10:53:19 +02:00
|
|
|
|
2021-03-02 11:41:04 +01:00
|
|
|
const db = await CoreSites.getSiteDb(siteId);
|
2020-10-08 16:33:10 +02:00
|
|
|
|
2020-10-28 14:25:18 +01:00
|
|
|
await db.insertRecord(FILES_TABLE_NAME, record);
|
2020-10-07 10:53:19 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Adds a hash to a filename if needed.
|
|
|
|
*
|
|
|
|
* @param url The URL of the file, already treated (decoded, without revision, etc.).
|
|
|
|
* @param filename The filename.
|
|
|
|
* @return The filename with the hash.
|
|
|
|
*/
|
|
|
|
protected addHashToFilename(url: string, filename: string): string {
|
|
|
|
// Check if the file already has a hash. If a file is downloaded and re-uploaded with the app it will have a hash already.
|
|
|
|
const matches = filename.match(/_[a-f0-9]{32}/g);
|
|
|
|
|
|
|
|
if (matches && matches.length) {
|
|
|
|
// There is at least 1 match. Get the last one.
|
|
|
|
const hash = matches[matches.length - 1];
|
|
|
|
const treatedUrl = url.replace(hash, ''); // Remove the hash from the URL.
|
|
|
|
|
|
|
|
// Check that the hash is valid.
|
|
|
|
if ('_' + Md5.hashAsciiStr('url:' + treatedUrl) == hash) {
|
|
|
|
// The data found is a hash of the URL, don't need to add it again.
|
|
|
|
return filename;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
return filename + '_' + Md5.hashAsciiStr('url:' + url);
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Add a file to the queue.
|
|
|
|
*
|
|
|
|
* @param siteId The site ID.
|
|
|
|
* @param fileId The file ID.
|
|
|
|
* @param url The absolute URL to the file.
|
|
|
|
* @param priority The priority this file should get in the queue (range 0-999).
|
|
|
|
* @param revision The revision of the file.
|
|
|
|
* @param timemodified The time this file was modified. Can be used to check file state.
|
|
|
|
* @param filePath Filepath to download the file to. If not defined, download to the filepool folder.
|
2020-10-08 16:33:10 +02:00
|
|
|
* @param onProgress Function to call on progress.
|
2020-10-07 10:53:19 +02:00
|
|
|
* @param options Extra options (isexternalfile, repositorytype).
|
|
|
|
* @param link The link to add for the file.
|
|
|
|
* @return Promise resolved when the file is downloaded.
|
|
|
|
*/
|
2020-10-21 16:32:27 +02:00
|
|
|
protected async addToQueue(
|
|
|
|
siteId: string,
|
|
|
|
fileId: string,
|
|
|
|
url: string,
|
|
|
|
priority: number,
|
|
|
|
revision: number,
|
|
|
|
timemodified: number,
|
|
|
|
filePath?: string,
|
|
|
|
onProgress?: CoreFilepoolOnProgressCallback,
|
|
|
|
options: CoreFilepoolFileOptions = {},
|
|
|
|
link?: CoreFilepoolComponentLink,
|
|
|
|
): Promise<void> {
|
2020-10-07 10:53:19 +02:00
|
|
|
this.logger.debug(`Adding ${fileId} to the queue`);
|
|
|
|
|
2020-12-01 18:37:24 +01:00
|
|
|
const db = await this.appDB;
|
|
|
|
|
|
|
|
await db.insertRecord(QUEUE_TABLE_NAME, {
|
2020-10-07 10:53:19 +02:00
|
|
|
siteId,
|
|
|
|
fileId,
|
|
|
|
url,
|
|
|
|
priority,
|
|
|
|
revision,
|
|
|
|
timemodified,
|
|
|
|
path: filePath,
|
|
|
|
isexternalfile: options.isexternalfile ? 1 : 0,
|
|
|
|
repositorytype: options.repositorytype,
|
|
|
|
links: JSON.stringify(link ? [link] : []),
|
2020-10-08 16:33:10 +02:00
|
|
|
added: Date.now(),
|
2020-10-07 10:53:19 +02:00
|
|
|
});
|
|
|
|
|
|
|
|
// Check if the queue is running.
|
|
|
|
this.checkQueueProcessing();
|
|
|
|
this.notifyFileDownloading(siteId, fileId, link ? [link] : []);
|
|
|
|
|
|
|
|
return this.getQueuePromise(siteId, fileId, true, onProgress);
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Add an entry to queue using a URL.
|
|
|
|
*
|
|
|
|
* @param siteId The site ID.
|
|
|
|
* @param fileUrl The absolute URL to the file.
|
|
|
|
* @param component The component to link the file to.
|
|
|
|
* @param componentId An ID to use in conjunction with the component (optional).
|
|
|
|
* @param timemodified The time this file was modified. Can be used to check file state.
|
|
|
|
* @param filePath Filepath to download the file to. If not defined, download to the filepool folder.
|
|
|
|
* @param onProgress Function to call on progress.
|
|
|
|
* @param priority The priority this file should get in the queue (range 0-999).
|
|
|
|
* @param options Extra options (isexternalfile, repositorytype).
|
|
|
|
* @param revision File revision. If not defined, it will be calculated using the URL.
|
|
|
|
* @param alreadyFixed Whether the URL has already been fixed.
|
|
|
|
* @return Resolved on success.
|
|
|
|
*/
|
2020-10-21 16:32:27 +02:00
|
|
|
async addToQueueByUrl(
|
|
|
|
siteId: string,
|
|
|
|
fileUrl: string,
|
|
|
|
component?: string,
|
|
|
|
componentId?: string | number,
|
|
|
|
timemodified: number = 0,
|
|
|
|
filePath?: string,
|
|
|
|
onProgress?: CoreFilepoolOnProgressCallback,
|
|
|
|
priority: number = 0,
|
|
|
|
options: CoreFilepoolFileOptions = {},
|
|
|
|
revision?: number,
|
|
|
|
alreadyFixed?: boolean,
|
|
|
|
): Promise<void> {
|
2021-03-02 11:41:04 +01:00
|
|
|
if (!CoreFile.isAvailable()) {
|
2020-10-14 16:38:24 +02:00
|
|
|
throw new CoreError('File system cannot be used.');
|
2020-10-07 10:53:19 +02:00
|
|
|
}
|
|
|
|
|
2021-03-02 11:41:04 +01:00
|
|
|
const site = await CoreSites.getSite(siteId);
|
2020-10-08 16:33:10 +02:00
|
|
|
if (!site.canDownloadFiles()) {
|
2020-10-14 16:38:24 +02:00
|
|
|
throw new CoreError('Site doesn\'t allow downloading files.');
|
2020-10-08 16:33:10 +02:00
|
|
|
}
|
2020-10-07 10:53:19 +02:00
|
|
|
|
2020-10-21 16:32:27 +02:00
|
|
|
if (!alreadyFixed) {
|
|
|
|
// Fix the URL and use the fixed data.
|
|
|
|
const file = await this.fixPluginfileURL(siteId, fileUrl);
|
|
|
|
|
|
|
|
fileUrl = file.fileurl;
|
|
|
|
timemodified = file.timemodified || timemodified;
|
2020-10-08 16:33:10 +02:00
|
|
|
}
|
2020-10-07 10:53:19 +02:00
|
|
|
|
2020-10-08 16:33:10 +02:00
|
|
|
revision = revision || this.getRevisionFromUrl(fileUrl);
|
|
|
|
const fileId = this.getFileIdByUrl(fileUrl);
|
2020-10-07 10:53:19 +02:00
|
|
|
|
2020-10-08 16:33:10 +02:00
|
|
|
const primaryKey = { siteId, fileId };
|
2020-10-07 10:53:19 +02:00
|
|
|
|
2020-10-08 16:33:10 +02:00
|
|
|
// Set up the component.
|
|
|
|
const link = this.createComponentLink(component, componentId);
|
2020-10-07 10:53:19 +02:00
|
|
|
|
2020-10-08 16:33:10 +02:00
|
|
|
// Retrieve the queue deferred now if it exists.
|
|
|
|
// This is to prevent errors if file is removed from queue while we're checking if the file is in queue.
|
|
|
|
const queueDeferred = this.getQueueDeferred(siteId, fileId, false, onProgress);
|
2020-10-21 16:32:27 +02:00
|
|
|
let entry: CoreFilepoolQueueEntry;
|
2020-10-07 10:53:19 +02:00
|
|
|
|
2020-10-21 16:32:27 +02:00
|
|
|
try {
|
|
|
|
entry = await this.hasFileInQueue(siteId, fileId);
|
|
|
|
} catch (error) {
|
|
|
|
// Unsure why we could not get the record, let's add to the queue anyway.
|
|
|
|
return this.addToQueue(siteId, fileId, fileUrl, priority, revision, timemodified, filePath, onProgress, options, link);
|
|
|
|
}
|
2020-10-07 10:53:19 +02:00
|
|
|
|
2020-10-21 16:32:27 +02:00
|
|
|
const newData: Partial<CoreFilepoolQueueDBEntry> = {};
|
|
|
|
let foundLink = false;
|
2020-10-07 10:53:19 +02:00
|
|
|
|
2020-10-21 16:32:27 +02:00
|
|
|
// We already have the file in queue, we update the priority and links.
|
|
|
|
if (!entry.priority || entry.priority < priority) {
|
|
|
|
newData.priority = priority;
|
|
|
|
}
|
|
|
|
if (revision && entry.revision !== revision) {
|
|
|
|
newData.revision = revision;
|
|
|
|
}
|
|
|
|
if (timemodified && entry.timemodified !== timemodified) {
|
|
|
|
newData.timemodified = timemodified;
|
|
|
|
}
|
|
|
|
if (filePath && entry.path !== filePath) {
|
|
|
|
newData.path = filePath;
|
|
|
|
}
|
|
|
|
if (entry.isexternalfile !== options.isexternalfile && (entry.isexternalfile || options.isexternalfile)) {
|
|
|
|
newData.isexternalfile = options.isexternalfile;
|
|
|
|
}
|
|
|
|
if (entry.repositorytype !== options.repositorytype && (entry.repositorytype || options.repositorytype)) {
|
|
|
|
newData.repositorytype = options.repositorytype;
|
|
|
|
}
|
2020-10-07 10:53:19 +02:00
|
|
|
|
2020-10-21 16:32:27 +02:00
|
|
|
if (link) {
|
|
|
|
// We need to add the new link if it does not exist yet.
|
|
|
|
if (entry.linksUnserialized && entry.linksUnserialized.length) {
|
|
|
|
foundLink = entry.linksUnserialized.some((fileLink) =>
|
|
|
|
fileLink.component == link.component && fileLink.componentId == link.componentId);
|
|
|
|
}
|
2020-10-07 10:53:19 +02:00
|
|
|
|
2020-10-21 16:32:27 +02:00
|
|
|
if (!foundLink) {
|
|
|
|
const links = entry.linksUnserialized || [];
|
|
|
|
links.push(link);
|
|
|
|
newData.links = JSON.stringify(links);
|
|
|
|
}
|
|
|
|
}
|
2020-10-07 10:53:19 +02:00
|
|
|
|
2020-10-21 16:32:27 +02:00
|
|
|
if (Object.keys(newData).length) {
|
|
|
|
// Update only when required.
|
|
|
|
this.logger.debug(`Updating file ${fileId} which is already in queue`);
|
2020-10-07 10:53:19 +02:00
|
|
|
|
2020-12-01 18:37:24 +01:00
|
|
|
const db = await this.appDB;
|
|
|
|
|
|
|
|
return db.updateRecords(QUEUE_TABLE_NAME, newData, primaryKey).then(() =>
|
2020-10-21 16:32:27 +02:00
|
|
|
this.getQueuePromise(siteId, fileId, true, onProgress));
|
|
|
|
}
|
|
|
|
|
|
|
|
this.logger.debug(`File ${fileId} already in queue and does not require update`);
|
|
|
|
if (queueDeferred) {
|
|
|
|
// If we were able to retrieve the queue deferred before, we use that one.
|
|
|
|
return queueDeferred.promise;
|
|
|
|
} else {
|
|
|
|
// Create a new deferred and return its promise.
|
|
|
|
return this.getQueuePromise(siteId, fileId, true, onProgress);
|
|
|
|
}
|
2020-10-07 10:53:19 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Adds a file to the queue if the size is allowed to be downloaded.
|
|
|
|
*
|
|
|
|
* @param siteId The site ID.
|
|
|
|
* @param fileUrl The absolute URL to the file, already fixed.
|
|
|
|
* @param component The component to link the file to.
|
|
|
|
* @param componentId An ID to use in conjunction with the component.
|
|
|
|
* @param timemodified The time this file was modified.
|
|
|
|
* @param checkSize True if we shouldn't download files if their size is big, false otherwise.
|
2020-10-14 16:38:24 +02:00
|
|
|
* @param downloadUnknown True to download file in WiFi if their size is unknown, false otherwise.
|
2020-10-07 10:53:19 +02:00
|
|
|
* Ignored if checkSize=false.
|
|
|
|
* @param options Extra options (isexternalfile, repositorytype).
|
|
|
|
* @param revision File revision. If not defined, it will be calculated using the URL.
|
|
|
|
* @return Promise resolved when the file is downloaded.
|
|
|
|
*/
|
2020-10-21 16:32:27 +02:00
|
|
|
protected async addToQueueIfNeeded(
|
|
|
|
siteId: string,
|
|
|
|
fileUrl: string,
|
|
|
|
component?: string,
|
|
|
|
componentId?: string | number,
|
|
|
|
timemodified: number = 0,
|
|
|
|
checkSize: boolean = true,
|
|
|
|
downloadUnknown?: boolean,
|
|
|
|
options: CoreFilepoolFileOptions = {},
|
|
|
|
revision?: number,
|
|
|
|
): Promise<void> {
|
2020-10-08 16:33:10 +02:00
|
|
|
if (!checkSize) {
|
|
|
|
// No need to check size, just add it to the queue.
|
2020-10-21 16:32:27 +02:00
|
|
|
await this.addToQueueByUrl(
|
|
|
|
siteId,
|
|
|
|
fileUrl,
|
|
|
|
component,
|
|
|
|
componentId,
|
|
|
|
timemodified,
|
|
|
|
undefined,
|
|
|
|
undefined,
|
|
|
|
0,
|
|
|
|
options,
|
|
|
|
revision,
|
|
|
|
true,
|
|
|
|
);
|
2020-10-08 16:33:10 +02:00
|
|
|
}
|
2020-10-07 10:53:19 +02:00
|
|
|
|
2020-10-08 16:33:10 +02:00
|
|
|
let size: number;
|
2020-10-07 10:53:19 +02:00
|
|
|
|
2020-10-08 16:33:10 +02:00
|
|
|
if (typeof this.sizeCache[fileUrl] != 'undefined') {
|
|
|
|
size = this.sizeCache[fileUrl];
|
|
|
|
} else {
|
2021-03-02 11:41:04 +01:00
|
|
|
if (!CoreApp.isOnline()) {
|
2020-10-08 16:33:10 +02:00
|
|
|
// Cannot check size in offline, stop.
|
2021-03-02 11:41:04 +01:00
|
|
|
throw new CoreError(Translate.instant('core.cannotconnect'));
|
2020-10-07 10:53:19 +02:00
|
|
|
}
|
|
|
|
|
2021-03-02 11:41:04 +01:00
|
|
|
size = await CoreWS.getRemoteFileSize(fileUrl);
|
2020-10-08 16:33:10 +02:00
|
|
|
}
|
2020-10-07 10:53:19 +02:00
|
|
|
|
2020-10-08 16:33:10 +02:00
|
|
|
// Calculate the size of the file.
|
2021-03-02 11:41:04 +01:00
|
|
|
const isWifi = CoreApp.isWifi();
|
2020-10-14 16:38:24 +02:00
|
|
|
const sizeUnknown = size <= 0;
|
2020-10-07 10:53:19 +02:00
|
|
|
|
2020-10-14 16:38:24 +02:00
|
|
|
if (!sizeUnknown) {
|
2020-10-08 16:33:10 +02:00
|
|
|
// Store the size in the cache.
|
|
|
|
this.sizeCache[fileUrl] = size;
|
|
|
|
}
|
|
|
|
|
|
|
|
// Check if the file should be downloaded.
|
2020-10-21 16:32:27 +02:00
|
|
|
if ((sizeUnknown && downloadUnknown && isWifi) || (!sizeUnknown && this.shouldDownload(size))) {
|
|
|
|
await this.addToQueueByUrl(
|
|
|
|
siteId,
|
|
|
|
fileUrl,
|
|
|
|
component,
|
|
|
|
componentId,
|
|
|
|
timemodified,
|
|
|
|
undefined,
|
|
|
|
undefined,
|
|
|
|
0,
|
|
|
|
options,
|
|
|
|
revision,
|
|
|
|
true,
|
|
|
|
);
|
2020-10-07 10:53:19 +02:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Check the queue processing.
|
|
|
|
*
|
|
|
|
* @description
|
|
|
|
* In mose cases, this will enable the queue processing if it was paused.
|
|
|
|
* Though, this will disable the queue if we are missing network or if the file system
|
|
|
|
* is not accessible. Also, this will have no effect if the queue is already running.
|
|
|
|
*/
|
|
|
|
protected checkQueueProcessing(): void {
|
2021-03-02 11:41:04 +01:00
|
|
|
if (!CoreFile.isAvailable() || !CoreApp.isOnline()) {
|
2020-10-08 16:33:10 +02:00
|
|
|
this.queueState = CoreFilepoolProvider.QUEUE_PAUSED;
|
2020-10-07 10:53:19 +02:00
|
|
|
|
|
|
|
return;
|
2020-10-08 16:33:10 +02:00
|
|
|
} else if (this.queueState === CoreFilepoolProvider.QUEUE_RUNNING) {
|
2020-10-07 10:53:19 +02:00
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
2020-10-08 16:33:10 +02:00
|
|
|
this.queueState = CoreFilepoolProvider.QUEUE_RUNNING;
|
2020-10-07 10:53:19 +02:00
|
|
|
this.processQueue();
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Clear all packages status in a site.
|
|
|
|
*
|
|
|
|
* @param siteId Site ID.
|
|
|
|
* @return Promise resolved when all status are cleared.
|
|
|
|
*/
|
2020-10-08 16:33:10 +02:00
|
|
|
async clearAllPackagesStatus(siteId: string): Promise<void> {
|
2020-10-07 10:53:19 +02:00
|
|
|
this.logger.debug('Clear all packages status for site ' + siteId);
|
|
|
|
|
2021-03-02 11:41:04 +01:00
|
|
|
const site = await CoreSites.getSite(siteId);
|
2020-10-08 16:33:10 +02:00
|
|
|
// Get all the packages to be able to "notify" the change in the status.
|
2020-10-28 14:25:18 +01:00
|
|
|
const entries: CoreFilepoolPackageEntry[] = await site.getDb().getAllRecords(PACKAGES_TABLE_NAME);
|
2020-10-08 16:33:10 +02:00
|
|
|
// Delete all the entries.
|
2020-10-28 14:25:18 +01:00
|
|
|
await site.getDb().deleteRecords(PACKAGES_TABLE_NAME);
|
2020-10-08 16:33:10 +02:00
|
|
|
|
|
|
|
entries.forEach((entry) => {
|
|
|
|
// Trigger module status changed, setting it as not downloaded.
|
2020-12-11 15:40:34 +01:00
|
|
|
this.triggerPackageStatusChanged(siteId, CoreConstants.NOT_DOWNLOADED, entry.component!, entry.componentId);
|
2020-10-07 10:53:19 +02:00
|
|
|
});
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Clears the filepool. Use it only when all the files from a site are deleted.
|
|
|
|
*
|
|
|
|
* @param siteId ID of the site to clear.
|
|
|
|
* @return Promise resolved when the filepool is cleared.
|
|
|
|
*/
|
2020-10-08 16:33:10 +02:00
|
|
|
async clearFilepool(siteId: string): Promise<void> {
|
2021-03-02 11:41:04 +01:00
|
|
|
const db = await CoreSites.getSiteDb(siteId);
|
2020-10-08 16:33:10 +02:00
|
|
|
|
|
|
|
await Promise.all([
|
2020-10-28 14:25:18 +01:00
|
|
|
db.deleteRecords(FILES_TABLE_NAME),
|
|
|
|
db.deleteRecords(LINKS_TABLE_NAME),
|
2020-10-08 16:33:10 +02:00
|
|
|
]);
|
2020-10-07 10:53:19 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Returns whether a component has files in the pool.
|
|
|
|
*
|
|
|
|
* @param siteId The site ID.
|
|
|
|
* @param component The component to link the file to.
|
|
|
|
* @param componentId An ID to use in conjunction with the component.
|
|
|
|
* @return Resolved means yes, rejected means no.
|
|
|
|
*/
|
2020-10-08 16:33:10 +02:00
|
|
|
async componentHasFiles(siteId: string, component: string, componentId?: string | number): Promise<void> {
|
2021-03-02 11:41:04 +01:00
|
|
|
const db = await CoreSites.getSiteDb(siteId);
|
2020-10-08 16:33:10 +02:00
|
|
|
const conditions = {
|
|
|
|
component,
|
|
|
|
componentId: this.fixComponentId(componentId),
|
|
|
|
};
|
2020-10-07 10:53:19 +02:00
|
|
|
|
2020-10-28 14:25:18 +01:00
|
|
|
const count = await db.countRecords(LINKS_TABLE_NAME, conditions);
|
2020-10-08 16:33:10 +02:00
|
|
|
if (count <= 0) {
|
2020-10-14 16:38:24 +02:00
|
|
|
throw new CoreError('Component doesn\'t have files');
|
2020-10-08 16:33:10 +02:00
|
|
|
}
|
2020-10-07 10:53:19 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Prepare a component link.
|
|
|
|
*
|
|
|
|
* @param component The component to link the file to.
|
|
|
|
* @param componentId An ID to use in conjunction with the component.
|
|
|
|
* @return Link, null if nothing to link.
|
|
|
|
*/
|
2020-10-21 16:32:27 +02:00
|
|
|
protected createComponentLink(component?: string, componentId?: string | number): CoreFilepoolComponentLink | undefined {
|
2020-10-07 10:53:19 +02:00
|
|
|
if (typeof component != 'undefined' && component != null) {
|
|
|
|
return { component, componentId: this.fixComponentId(componentId) };
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Prepare list of links from component and componentId.
|
|
|
|
*
|
|
|
|
* @param component The component to link the file to.
|
|
|
|
* @param componentId An ID to use in conjunction with the component.
|
|
|
|
* @return Links.
|
|
|
|
*/
|
2020-10-21 16:32:27 +02:00
|
|
|
protected createComponentLinks(component?: string, componentId?: string | number): CoreFilepoolComponentLink[] {
|
2020-10-07 10:53:19 +02:00
|
|
|
const link = this.createComponentLink(component, componentId);
|
|
|
|
|
|
|
|
return link ? [link] : [];
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Given the current status of a list of packages and the status of one of the packages,
|
|
|
|
* determine the new status for the list of packages. The status of a list of packages is:
|
|
|
|
* - CoreConstants.NOT_DOWNLOADABLE if there are no downloadable packages.
|
|
|
|
* - CoreConstants.NOT_DOWNLOADED if at least 1 package has status CoreConstants.NOT_DOWNLOADED.
|
|
|
|
* - CoreConstants.DOWNLOADED if ALL the downloadable packages have status CoreConstants.DOWNLOADED.
|
|
|
|
* - CoreConstants.DOWNLOADING if ALL the downloadable packages have status CoreConstants.DOWNLOADING or
|
|
|
|
* CoreConstants.DOWNLOADED, with at least 1 package with CoreConstants.DOWNLOADING.
|
|
|
|
* - CoreConstants.OUTDATED if ALL the downloadable packages have status CoreConstants.OUTDATED or CoreConstants.DOWNLOADED
|
|
|
|
* or CoreConstants.DOWNLOADING, with at least 1 package with CoreConstants.OUTDATED.
|
|
|
|
*
|
|
|
|
* @param current Current status of the list of packages.
|
|
|
|
* @param packagestatus Status of one of the packages.
|
|
|
|
* @return New status for the list of packages;
|
|
|
|
*/
|
|
|
|
determinePackagesStatus(current: string, packageStatus: string): string {
|
|
|
|
if (!current) {
|
|
|
|
current = CoreConstants.NOT_DOWNLOADABLE;
|
|
|
|
}
|
|
|
|
|
|
|
|
if (packageStatus === CoreConstants.NOT_DOWNLOADED) {
|
|
|
|
// If 1 package is not downloaded the status of the whole list will always be not downloaded.
|
|
|
|
return CoreConstants.NOT_DOWNLOADED;
|
|
|
|
} else if (packageStatus === CoreConstants.DOWNLOADED && current === CoreConstants.NOT_DOWNLOADABLE) {
|
|
|
|
// If all packages are downloaded or not downloadable with at least 1 downloaded, status will be downloaded.
|
|
|
|
return CoreConstants.DOWNLOADED;
|
|
|
|
} else if (packageStatus === CoreConstants.DOWNLOADING &&
|
|
|
|
(current === CoreConstants.NOT_DOWNLOADABLE || current === CoreConstants.DOWNLOADED)) {
|
|
|
|
// If all packages are downloading/downloaded/notdownloadable with at least 1 downloading, status will be downloading.
|
|
|
|
return CoreConstants.DOWNLOADING;
|
|
|
|
} else if (packageStatus === CoreConstants.OUTDATED && current !== CoreConstants.NOT_DOWNLOADED) {
|
|
|
|
// If there are no packages notdownloaded and there is at least 1 outdated, status will be outdated.
|
|
|
|
return CoreConstants.OUTDATED;
|
|
|
|
}
|
|
|
|
|
|
|
|
// Status remains the same.
|
|
|
|
return current;
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Downloads a URL and update or add it to the pool.
|
|
|
|
*
|
|
|
|
* This uses the file system, you should always make sure that it is accessible before calling this method.
|
|
|
|
*
|
|
|
|
* @param siteId The site ID.
|
|
|
|
* @param fileUrl The file URL.
|
|
|
|
* @param options Extra options (revision, timemodified, isexternalfile, repositorytype).
|
|
|
|
* @param filePath Filepath to download the file to. If defined, no extension will be added.
|
|
|
|
* @param onProgress Function to call on progress.
|
|
|
|
* @param poolFileObject When set, the object will be updated, a new entry will not be created.
|
|
|
|
* @return Resolved with internal URL on success, rejected otherwise.
|
|
|
|
*/
|
2020-10-21 16:32:27 +02:00
|
|
|
protected async downloadForPoolByUrl(
|
|
|
|
siteId: string,
|
|
|
|
fileUrl: string,
|
|
|
|
options: CoreFilepoolFileOptions = {},
|
|
|
|
filePath?: string,
|
|
|
|
onProgress?: CoreFilepoolOnProgressCallback,
|
|
|
|
poolFileObject?: CoreFilepoolFileEntry,
|
|
|
|
): Promise<string> {
|
2020-10-07 10:53:19 +02:00
|
|
|
const fileId = this.getFileIdByUrl(fileUrl);
|
2021-03-02 11:41:04 +01:00
|
|
|
const extension = CoreMimetypeUtils.guessExtensionFromUrl(fileUrl);
|
2020-10-07 10:53:19 +02:00
|
|
|
const addExtension = typeof filePath == 'undefined';
|
2020-10-21 16:32:27 +02:00
|
|
|
const path = filePath || (await this.getFilePath(siteId, fileId, extension));
|
2020-10-07 10:53:19 +02:00
|
|
|
|
2020-10-08 16:33:10 +02:00
|
|
|
if (poolFileObject && poolFileObject.fileId !== fileId) {
|
|
|
|
this.logger.error('Invalid object to update passed');
|
2020-10-07 10:53:19 +02:00
|
|
|
|
2020-10-14 16:38:24 +02:00
|
|
|
throw new CoreError('Invalid object to update passed.');
|
2020-10-08 16:33:10 +02:00
|
|
|
}
|
2020-10-07 10:53:19 +02:00
|
|
|
|
2020-10-21 16:32:27 +02:00
|
|
|
const downloadId = this.getFileDownloadId(fileUrl, path);
|
2020-10-07 10:53:19 +02:00
|
|
|
|
2020-10-08 16:33:10 +02:00
|
|
|
if (this.filePromises[siteId] && this.filePromises[siteId][downloadId]) {
|
|
|
|
// There's already a download ongoing for this file in this location, return the promise.
|
|
|
|
return this.filePromises[siteId][downloadId];
|
|
|
|
} else if (!this.filePromises[siteId]) {
|
|
|
|
this.filePromises[siteId] = {};
|
|
|
|
}
|
2020-10-07 10:53:19 +02:00
|
|
|
|
2021-03-02 11:41:04 +01:00
|
|
|
this.filePromises[siteId][downloadId] = CoreSites.getSite(siteId).then(async (site) => {
|
2020-10-08 16:33:10 +02:00
|
|
|
if (!site.canDownloadFiles()) {
|
2020-10-14 16:38:24 +02:00
|
|
|
throw new CoreError('Site doesn\'t allow downloading files.');
|
2020-10-08 16:33:10 +02:00
|
|
|
}
|
2020-10-07 10:53:19 +02:00
|
|
|
|
2021-03-02 11:41:04 +01:00
|
|
|
const entry = await CoreWS.downloadFile(fileUrl, path, addExtension, onProgress);
|
2020-10-08 16:33:10 +02:00
|
|
|
const fileEntry = entry;
|
2021-03-02 11:41:04 +01:00
|
|
|
await CorePluginFileDelegate.treatDownloadedFile(fileUrl, fileEntry, siteId, onProgress);
|
2020-10-07 10:53:19 +02:00
|
|
|
|
2020-10-21 16:32:27 +02:00
|
|
|
await this.addFileToPool(siteId, fileId, {
|
|
|
|
downloadTime: Date.now(),
|
|
|
|
stale: 0,
|
|
|
|
url: fileUrl,
|
|
|
|
revision: options.revision,
|
|
|
|
timemodified: options.timemodified,
|
|
|
|
isexternalfile: options.isexternalfile ? 1 : 0,
|
|
|
|
repositorytype: options.repositorytype,
|
|
|
|
path: fileEntry.path,
|
|
|
|
extension: fileEntry.extension,
|
|
|
|
});
|
2020-10-07 10:53:19 +02:00
|
|
|
|
2020-10-08 16:33:10 +02:00
|
|
|
return fileEntry.toURL();
|
|
|
|
}).finally(() => {
|
|
|
|
// Download finished, delete the promise.
|
|
|
|
delete this.filePromises[siteId][downloadId];
|
2020-10-07 10:53:19 +02:00
|
|
|
});
|
2020-10-08 16:33:10 +02:00
|
|
|
|
|
|
|
return this.filePromises[siteId][downloadId];
|
2020-10-07 10:53:19 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Download or prefetch several files into the filepool folder.
|
|
|
|
*
|
|
|
|
* @param siteId The site ID.
|
|
|
|
* @param files Array of files to download.
|
|
|
|
* @param prefetch True if should prefetch the contents (queue), false if they should be downloaded right now.
|
|
|
|
* @param ignoreStale True if 'stale' should be ignored. Only if prefetch=false.
|
|
|
|
* @param component The component to link the file to.
|
|
|
|
* @param componentId An ID to use in conjunction with the component.
|
|
|
|
* @param dirPath Name of the directory where to store the files (inside filepool dir). If not defined, store
|
|
|
|
* the files directly inside the filepool folder.
|
|
|
|
* @return Resolved on success.
|
|
|
|
*/
|
2020-10-21 16:32:27 +02:00
|
|
|
downloadOrPrefetchFiles(
|
|
|
|
siteId: string,
|
|
|
|
files: CoreWSExternalFile[],
|
|
|
|
prefetch: boolean,
|
|
|
|
ignoreStale?: boolean,
|
|
|
|
component?: string,
|
|
|
|
componentId?: string | number,
|
|
|
|
dirPath?: string,
|
|
|
|
): Promise<void> {
|
|
|
|
const promises: Promise<unknown>[] = [];
|
2020-10-07 10:53:19 +02:00
|
|
|
|
|
|
|
// Download files.
|
|
|
|
files.forEach((file) => {
|
2020-10-08 16:33:10 +02:00
|
|
|
const url = file.fileurl;
|
2020-10-07 10:53:19 +02:00
|
|
|
const timemodified = file.timemodified;
|
|
|
|
const options = {
|
|
|
|
isexternalfile: file.isexternalfile,
|
|
|
|
repositorytype: file.repositorytype,
|
|
|
|
};
|
2020-10-21 16:32:27 +02:00
|
|
|
let path: string | undefined;
|
2020-10-07 10:53:19 +02:00
|
|
|
|
|
|
|
if (dirPath) {
|
|
|
|
// Calculate the path to the file.
|
|
|
|
path = file.filename;
|
2020-10-21 16:32:27 +02:00
|
|
|
if (file.filepath && file.filepath !== '/') {
|
2020-10-07 10:53:19 +02:00
|
|
|
path = file.filepath.substr(1) + path;
|
|
|
|
}
|
2021-03-02 11:41:04 +01:00
|
|
|
path = CoreTextUtils.concatenatePaths(dirPath, path!);
|
2020-10-07 10:53:19 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
if (prefetch) {
|
2020-10-21 16:32:27 +02:00
|
|
|
promises.push(this.addToQueueByUrl(siteId, url, component, componentId, timemodified, path, undefined, 0, options));
|
2020-10-07 10:53:19 +02:00
|
|
|
} else {
|
|
|
|
promises.push(this.downloadUrl(
|
2020-10-21 16:32:27 +02:00
|
|
|
siteId,
|
|
|
|
url,
|
|
|
|
ignoreStale,
|
|
|
|
component,
|
|
|
|
componentId,
|
|
|
|
timemodified,
|
|
|
|
undefined,
|
|
|
|
path,
|
|
|
|
options,
|
|
|
|
));
|
2020-10-07 10:53:19 +02:00
|
|
|
}
|
|
|
|
});
|
|
|
|
|
2021-03-02 11:41:04 +01:00
|
|
|
return CoreUtils.allPromises(promises);
|
2020-10-07 10:53:19 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Downloads or prefetches a list of files as a "package".
|
|
|
|
*
|
|
|
|
* @param siteId The site ID.
|
|
|
|
* @param fileList List of files to download.
|
|
|
|
* @param prefetch True if should prefetch the contents (queue), false if they should be downloaded right now.
|
|
|
|
* @param component The component to link the file to.
|
|
|
|
* @param componentId An ID to use in conjunction with the component.
|
|
|
|
* @param extra Extra data to store for the package.
|
|
|
|
* @param dirPath Name of the directory where to store the files (inside filepool dir). If not defined, store
|
|
|
|
* the files directly inside the filepool folder.
|
|
|
|
* @param onProgress Function to call on progress.
|
|
|
|
* @return Promise resolved when the package is downloaded.
|
|
|
|
*/
|
2021-01-25 07:42:55 +01:00
|
|
|
downloadOrPrefetchPackage(
|
2020-10-21 16:32:27 +02:00
|
|
|
siteId: string,
|
|
|
|
fileList: CoreWSExternalFile[],
|
|
|
|
prefetch: boolean,
|
|
|
|
component: string,
|
|
|
|
componentId?: string | number,
|
|
|
|
extra?: string,
|
|
|
|
dirPath?: string,
|
|
|
|
onProgress?: CoreFilepoolOnProgressCallback,
|
|
|
|
): Promise<void> {
|
2020-10-07 10:53:19 +02:00
|
|
|
const packageId = this.getPackageId(component, componentId);
|
|
|
|
|
|
|
|
if (this.packagesPromises[siteId] && this.packagesPromises[siteId][packageId]) {
|
|
|
|
// There's already a download ongoing for this package, return the promise.
|
|
|
|
return this.packagesPromises[siteId][packageId];
|
|
|
|
} else if (!this.packagesPromises[siteId]) {
|
|
|
|
this.packagesPromises[siteId] = {};
|
|
|
|
}
|
|
|
|
|
|
|
|
// Set package as downloading.
|
2020-10-08 16:33:10 +02:00
|
|
|
const promise = this.storePackageStatus(siteId, CoreConstants.DOWNLOADING, component, componentId).then(async () => {
|
2020-10-21 16:32:27 +02:00
|
|
|
const promises: Promise<string | void>[] = [];
|
2020-10-07 10:53:19 +02:00
|
|
|
let packageLoaded = 0;
|
|
|
|
|
|
|
|
fileList.forEach((file) => {
|
2020-10-08 16:33:10 +02:00
|
|
|
const fileUrl = file.fileurl;
|
2020-10-07 10:53:19 +02:00
|
|
|
const options = {
|
|
|
|
isexternalfile: file.isexternalfile,
|
|
|
|
repositorytype: file.repositorytype,
|
|
|
|
};
|
2020-10-21 16:32:27 +02:00
|
|
|
let path: string | undefined;
|
2020-10-08 16:33:10 +02:00
|
|
|
let promise: Promise<string | void>;
|
2020-10-07 10:53:19 +02:00
|
|
|
let fileLoaded = 0;
|
2020-10-21 16:32:27 +02:00
|
|
|
let onFileProgress: ((progress: ProgressEvent) => void) | undefined;
|
2020-10-07 10:53:19 +02:00
|
|
|
|
|
|
|
if (onProgress) {
|
|
|
|
// There's a onProgress event, create a function to receive file download progress events.
|
2020-10-08 16:33:10 +02:00
|
|
|
onFileProgress = (progress: ProgressEvent): void => {
|
2020-10-07 10:53:19 +02:00
|
|
|
if (progress && progress.loaded) {
|
|
|
|
// Add the new size loaded to the package loaded.
|
|
|
|
packageLoaded = packageLoaded + (progress.loaded - fileLoaded);
|
|
|
|
fileLoaded = progress.loaded;
|
|
|
|
onProgress({
|
|
|
|
packageDownload: true,
|
|
|
|
loaded: packageLoaded,
|
2020-10-08 16:33:10 +02:00
|
|
|
fileProgress: progress,
|
2020-10-07 10:53:19 +02:00
|
|
|
});
|
|
|
|
}
|
|
|
|
};
|
|
|
|
}
|
|
|
|
|
|
|
|
if (dirPath) {
|
|
|
|
// Calculate the path to the file.
|
|
|
|
path = file.filename;
|
2020-10-21 16:32:27 +02:00
|
|
|
if (file.filepath && file.filepath !== '/') {
|
2020-10-07 10:53:19 +02:00
|
|
|
path = file.filepath.substr(1) + path;
|
|
|
|
}
|
2021-03-02 11:41:04 +01:00
|
|
|
path = CoreTextUtils.concatenatePaths(dirPath, path!);
|
2020-10-07 10:53:19 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
if (prefetch) {
|
|
|
|
promise = this.addToQueueByUrl(
|
2020-10-21 16:32:27 +02:00
|
|
|
siteId,
|
|
|
|
fileUrl,
|
|
|
|
component,
|
|
|
|
componentId,
|
|
|
|
file.timemodified,
|
|
|
|
path,
|
|
|
|
undefined,
|
|
|
|
0,
|
|
|
|
options,
|
|
|
|
);
|
2020-10-07 10:53:19 +02:00
|
|
|
} else {
|
|
|
|
promise = this.downloadUrl(
|
2020-10-21 16:32:27 +02:00
|
|
|
siteId,
|
|
|
|
fileUrl,
|
|
|
|
false,
|
|
|
|
component,
|
|
|
|
componentId,
|
|
|
|
file.timemodified,
|
|
|
|
onFileProgress,
|
|
|
|
path,
|
|
|
|
options,
|
|
|
|
);
|
2020-10-07 10:53:19 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
// Using undefined for success & fail will pass the success/failure to the parent promise.
|
|
|
|
promises.push(promise);
|
|
|
|
});
|
|
|
|
|
2020-10-08 16:33:10 +02:00
|
|
|
try {
|
|
|
|
await Promise.all(promises);
|
2020-10-07 10:53:19 +02:00
|
|
|
// Success prefetching, store package as downloaded.
|
2020-10-14 16:38:24 +02:00
|
|
|
await this.storePackageStatus(siteId, CoreConstants.DOWNLOADED, component, componentId, extra);
|
2020-10-21 16:32:27 +02:00
|
|
|
|
|
|
|
return;
|
2020-10-08 16:33:10 +02:00
|
|
|
} catch (error) {
|
2020-10-07 10:53:19 +02:00
|
|
|
// Error downloading, go back to previous status and reject the promise.
|
2020-10-08 16:33:10 +02:00
|
|
|
await this.setPackagePreviousStatus(siteId, component, componentId);
|
2020-10-07 10:53:19 +02:00
|
|
|
|
2020-10-08 16:33:10 +02:00
|
|
|
throw error;
|
|
|
|
}
|
2020-10-07 10:53:19 +02:00
|
|
|
}).finally(() => {
|
|
|
|
// Download finished, delete the promise.
|
|
|
|
delete this.packagesPromises[siteId][packageId];
|
|
|
|
});
|
|
|
|
|
|
|
|
this.packagesPromises[siteId][packageId] = promise;
|
|
|
|
|
|
|
|
return promise;
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Downloads a list of files.
|
|
|
|
*
|
|
|
|
* @param siteId The site ID.
|
|
|
|
* @param fileList List of files to download.
|
|
|
|
* @param component The component to link the file to.
|
|
|
|
* @param componentId An ID to identify the download.
|
|
|
|
* @param extra Extra data to store for the package.
|
|
|
|
* @param dirPath Name of the directory where to store the files (inside filepool dir). If not defined, store
|
|
|
|
* the files directly inside the filepool folder.
|
|
|
|
* @param onProgress Function to call on progress.
|
|
|
|
* @return Promise resolved when all files are downloaded.
|
|
|
|
*/
|
2020-10-21 16:32:27 +02:00
|
|
|
downloadPackage(
|
|
|
|
siteId: string,
|
|
|
|
fileList: CoreWSExternalFile[],
|
|
|
|
component: string,
|
|
|
|
componentId?: string | number,
|
|
|
|
extra?: string,
|
|
|
|
dirPath?: string,
|
|
|
|
onProgress?: CoreFilepoolOnProgressCallback,
|
|
|
|
): Promise<void> {
|
2020-10-07 10:53:19 +02:00
|
|
|
return this.downloadOrPrefetchPackage(siteId, fileList, false, component, componentId, extra, dirPath, onProgress);
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Downloads a file on the spot.
|
|
|
|
*
|
|
|
|
* @param siteId The site ID.
|
|
|
|
* @param fileUrl The file URL.
|
|
|
|
* @param ignoreStale Whether 'stale' should be ignored.
|
|
|
|
* @param component The component to link the file to.
|
|
|
|
* @param componentId An ID to use in conjunction with the component.
|
|
|
|
* @param timemodified The time this file was modified. Can be used to check file state.
|
|
|
|
* @param filePath Filepath to download the file to. If not defined, download to the filepool folder.
|
|
|
|
* @param options Extra options (isexternalfile, repositorytype).
|
|
|
|
* @param revision File revision. If not defined, it will be calculated using the URL.
|
|
|
|
* @return Resolved with internal URL on success, rejected otherwise.
|
|
|
|
* @description
|
|
|
|
* Downloads a file on the spot.
|
|
|
|
*
|
|
|
|
* This will also take care of adding the file to the pool if it's missing. However, please note that this will
|
|
|
|
* not force a file to be re-downloaded if it is already part of the pool. You should mark a file as stale using
|
|
|
|
* invalidateFileByUrl to trigger a download.
|
|
|
|
*/
|
2020-10-21 16:32:27 +02:00
|
|
|
async downloadUrl(
|
|
|
|
siteId: string,
|
|
|
|
fileUrl: string,
|
|
|
|
ignoreStale?: boolean,
|
|
|
|
component?: string,
|
|
|
|
componentId?: string | number,
|
|
|
|
timemodified: number = 0,
|
|
|
|
onProgress?: CoreFilepoolOnProgressCallback,
|
|
|
|
filePath?: string,
|
|
|
|
options: CoreFilepoolFileOptions = {},
|
|
|
|
revision?: number,
|
|
|
|
): Promise<string> {
|
2020-10-07 10:53:19 +02:00
|
|
|
let alreadyDownloaded = true;
|
|
|
|
|
2021-03-02 11:41:04 +01:00
|
|
|
if (!CoreFile.isAvailable()) {
|
2020-10-14 16:38:24 +02:00
|
|
|
throw new CoreError('File system cannot be used.');
|
2020-10-08 16:33:10 +02:00
|
|
|
}
|
2020-10-07 10:53:19 +02:00
|
|
|
|
2020-10-08 16:33:10 +02:00
|
|
|
const file = await this.fixPluginfileURL(siteId, fileUrl);
|
|
|
|
fileUrl = file.fileurl;
|
|
|
|
timemodified = file.timemodified || timemodified;
|
2020-10-07 10:53:19 +02:00
|
|
|
|
2020-10-08 16:33:10 +02:00
|
|
|
options = Object.assign({}, options); // Create a copy to prevent modifying the original object.
|
|
|
|
options.timemodified = timemodified || 0;
|
|
|
|
options.revision = revision || this.getRevisionFromUrl(fileUrl);
|
|
|
|
const fileId = this.getFileIdByUrl(fileUrl);
|
2020-10-07 10:53:19 +02:00
|
|
|
|
2020-10-08 16:33:10 +02:00
|
|
|
const links = this.createComponentLinks(component, componentId);
|
2020-10-07 10:53:19 +02:00
|
|
|
|
2020-10-21 16:32:27 +02:00
|
|
|
const finishSuccessfulDownload = (url: string): string => {
|
|
|
|
if (typeof component != 'undefined') {
|
2021-03-02 11:41:04 +01:00
|
|
|
CoreUtils.ignoreErrors(this.addFileLink(siteId, fileId, component, componentId));
|
2020-10-21 16:32:27 +02:00
|
|
|
}
|
2020-10-07 10:53:19 +02:00
|
|
|
|
2020-10-21 16:32:27 +02:00
|
|
|
if (!alreadyDownloaded) {
|
|
|
|
this.notifyFileDownloaded(siteId, fileId, links);
|
|
|
|
}
|
2020-10-07 10:53:19 +02:00
|
|
|
|
2020-10-21 16:32:27 +02:00
|
|
|
return url;
|
|
|
|
};
|
|
|
|
|
|
|
|
try {
|
|
|
|
const fileObject = await this.hasFileInPool(siteId, fileId);
|
|
|
|
let url: string;
|
|
|
|
|
|
|
|
if (!fileObject ||
|
|
|
|
this.isFileOutdated(fileObject, options.revision, options.timemodified) &&
|
2021-03-02 11:41:04 +01:00
|
|
|
CoreApp.isOnline() &&
|
2020-10-21 16:32:27 +02:00
|
|
|
!ignoreStale
|
|
|
|
) {
|
|
|
|
throw new CoreError('Needs to be downloaded');
|
2020-10-08 16:33:10 +02:00
|
|
|
}
|
2020-10-07 10:53:19 +02:00
|
|
|
|
2020-10-21 16:32:27 +02:00
|
|
|
// File downloaded and not outdated, return the file from disk.
|
2020-10-08 16:33:10 +02:00
|
|
|
if (filePath) {
|
2020-10-21 16:32:27 +02:00
|
|
|
url = await this.getInternalUrlByPath(filePath);
|
2020-10-08 16:33:10 +02:00
|
|
|
} else {
|
2020-10-21 16:32:27 +02:00
|
|
|
url = await this.getInternalUrlById(siteId, fileId);
|
2020-10-08 16:33:10 +02:00
|
|
|
}
|
2020-10-07 10:53:19 +02:00
|
|
|
|
2020-10-21 16:32:27 +02:00
|
|
|
return finishSuccessfulDownload(url);
|
|
|
|
} catch (error) {
|
|
|
|
// The file is not downloaded or it's outdated.
|
2020-10-08 16:33:10 +02:00
|
|
|
this.notifyFileDownloading(siteId, fileId, links);
|
|
|
|
alreadyDownloaded = false;
|
|
|
|
|
2020-10-21 16:32:27 +02:00
|
|
|
try {
|
|
|
|
const url = await this.downloadForPoolByUrl(siteId, fileUrl, options, filePath, onProgress);
|
2020-10-07 10:53:19 +02:00
|
|
|
|
2020-10-21 16:32:27 +02:00
|
|
|
return finishSuccessfulDownload(url);
|
|
|
|
} catch (error) {
|
|
|
|
this.notifyFileDownloadError(siteId, fileId, links);
|
2020-10-07 10:53:19 +02:00
|
|
|
|
2020-10-21 16:32:27 +02:00
|
|
|
throw error;
|
|
|
|
}
|
|
|
|
}
|
2020-10-07 10:53:19 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Extract the downloadable URLs from an HTML code.
|
|
|
|
*
|
|
|
|
* @param html HTML code.
|
|
|
|
* @return List of file urls.
|
|
|
|
*/
|
|
|
|
extractDownloadableFilesFromHtml(html: string): string[] {
|
2020-10-21 16:32:27 +02:00
|
|
|
let urls: string[] = [];
|
2020-10-07 10:53:19 +02:00
|
|
|
|
2021-03-02 11:41:04 +01:00
|
|
|
const element = CoreDomUtils.convertToElement(html);
|
2020-10-21 16:32:27 +02:00
|
|
|
const elements: AnchorOrMediaElement[] = Array.from(element.querySelectorAll('a, img, audio, video, source, track'));
|
2020-10-07 10:53:19 +02:00
|
|
|
|
|
|
|
for (let i = 0; i < elements.length; i++) {
|
|
|
|
const element = elements[i];
|
2020-10-21 16:32:27 +02:00
|
|
|
const url = 'href' in element ? element.href : element.src;
|
2020-10-07 10:53:19 +02:00
|
|
|
|
2021-03-02 11:41:04 +01:00
|
|
|
if (url && CoreUrlUtils.isDownloadableUrl(url) && urls.indexOf(url) == -1) {
|
2020-10-07 10:53:19 +02:00
|
|
|
urls.push(url);
|
|
|
|
}
|
|
|
|
|
|
|
|
// Treat video poster.
|
|
|
|
if (element.tagName == 'VIDEO' && element.getAttribute('poster')) {
|
2020-10-21 16:32:27 +02:00
|
|
|
const poster = element.getAttribute('poster');
|
2021-03-02 11:41:04 +01:00
|
|
|
if (poster && CoreUrlUtils.isDownloadableUrl(poster) && urls.indexOf(poster) == -1) {
|
2020-10-21 16:32:27 +02:00
|
|
|
urls.push(poster);
|
2020-10-07 10:53:19 +02:00
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// Now get other files from plugin file handlers.
|
2021-03-02 11:41:04 +01:00
|
|
|
urls = urls.concat(CorePluginFileDelegate.getDownloadableFilesFromHTML(element));
|
2020-10-07 10:53:19 +02:00
|
|
|
|
|
|
|
return urls;
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Extract the downloadable URLs from an HTML code and returns them in fake file objects.
|
|
|
|
*
|
|
|
|
* @param html HTML code.
|
|
|
|
* @return List of fake file objects with file URLs.
|
|
|
|
*/
|
|
|
|
extractDownloadableFilesFromHtmlAsFakeFileObjects(html: string): CoreWSExternalFile[] {
|
|
|
|
const urls = this.extractDownloadableFilesFromHtml(html);
|
|
|
|
|
|
|
|
// Convert them to fake file objects.
|
2020-10-08 16:33:10 +02:00
|
|
|
return urls.map((url) => ({
|
|
|
|
fileurl: url,
|
|
|
|
}));
|
2020-10-07 10:53:19 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Fill Missing Extension In the File Object if needed.
|
|
|
|
* This is to migrate from old versions.
|
|
|
|
*
|
|
|
|
* @param fileObject File object to be migrated.
|
|
|
|
* @param siteId SiteID to get migrated.
|
|
|
|
* @return Promise resolved when done.
|
|
|
|
*/
|
2020-10-08 16:33:10 +02:00
|
|
|
protected async fillExtensionInFile(entry: CoreFilepoolFileEntry, siteId: string): Promise<void> {
|
2020-10-07 10:53:19 +02:00
|
|
|
if (typeof entry.extension != 'undefined') {
|
|
|
|
// Already filled.
|
2020-10-08 16:33:10 +02:00
|
|
|
return;
|
2020-10-07 10:53:19 +02:00
|
|
|
}
|
|
|
|
|
2021-03-02 11:41:04 +01:00
|
|
|
const db = await CoreSites.getSiteDb(siteId);
|
|
|
|
const extension = CoreMimetypeUtils.getFileExtension(entry.path);
|
2020-10-08 16:33:10 +02:00
|
|
|
if (!extension) {
|
|
|
|
// Files does not have extension. Invalidate file (stale = true).
|
|
|
|
// Minor problem: file will remain in the filesystem once downloaded again.
|
|
|
|
this.logger.debug('Staled file with no extension ' + entry.fileId);
|
2020-10-07 10:53:19 +02:00
|
|
|
|
2020-10-28 14:25:18 +01:00
|
|
|
await db.updateRecords(FILES_TABLE_NAME, { stale: 1 }, { fileId: entry.fileId });
|
2020-10-07 10:53:19 +02:00
|
|
|
|
2020-10-08 16:33:10 +02:00
|
|
|
return;
|
|
|
|
}
|
2020-10-07 10:53:19 +02:00
|
|
|
|
2020-10-08 16:33:10 +02:00
|
|
|
// File has extension. Save extension, and add extension to path.
|
|
|
|
const fileId = entry.fileId;
|
2021-03-02 11:41:04 +01:00
|
|
|
entry.fileId = CoreMimetypeUtils.removeExtension(fileId);
|
2020-10-08 16:33:10 +02:00
|
|
|
entry.extension = extension;
|
2020-10-07 10:53:19 +02:00
|
|
|
|
2020-10-28 14:25:18 +01:00
|
|
|
await db.updateRecords(FILES_TABLE_NAME, entry, { fileId });
|
2020-10-08 16:33:10 +02:00
|
|
|
if (entry.fileId == fileId) {
|
|
|
|
// File ID hasn't changed, we're done.
|
|
|
|
this.logger.debug('Removed extesion ' + extension + ' from file ' + entry.fileId);
|
2020-10-07 10:53:19 +02:00
|
|
|
|
2020-10-08 16:33:10 +02:00
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
// Now update the links.
|
2020-10-28 14:25:18 +01:00
|
|
|
await db.updateRecords(LINKS_TABLE_NAME, { fileId: entry.fileId }, { fileId });
|
2020-10-07 10:53:19 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Fix a component ID to always be a Number if possible.
|
|
|
|
*
|
|
|
|
* @param componentId The component ID.
|
|
|
|
* @return The normalised component ID. -1 when undefined was passed.
|
|
|
|
*/
|
2020-10-21 16:32:27 +02:00
|
|
|
protected fixComponentId(componentId?: string | number): string | number {
|
2020-10-07 10:53:19 +02:00
|
|
|
if (typeof componentId == 'number') {
|
|
|
|
return componentId;
|
|
|
|
}
|
|
|
|
|
2020-10-21 16:32:27 +02:00
|
|
|
if (typeof componentId == 'undefined' || componentId === null) {
|
|
|
|
return -1;
|
|
|
|
}
|
|
|
|
|
2020-10-07 10:53:19 +02:00
|
|
|
// Try to convert it to a number.
|
|
|
|
const id = parseInt(componentId, 10);
|
|
|
|
if (isNaN(id)) {
|
|
|
|
// Not a number.
|
2020-10-21 16:32:27 +02:00
|
|
|
return componentId;
|
2020-10-07 10:53:19 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
return id;
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Check whether the file can be downloaded, add the wstoken url and points to the correct script.
|
|
|
|
*
|
|
|
|
* @param siteId The site ID.
|
|
|
|
* @param fileUrl The file URL.
|
|
|
|
* @param timemodified The timemodified of the file.
|
|
|
|
* @return Promise resolved with the file data to use.
|
|
|
|
*/
|
2020-10-08 16:33:10 +02:00
|
|
|
protected async fixPluginfileURL(siteId: string, fileUrl: string, timemodified: number = 0): Promise<CoreWSExternalFile> {
|
2021-03-02 11:41:04 +01:00
|
|
|
const file = await CorePluginFileDelegate.getDownloadableFile({ fileurl: fileUrl, timemodified });
|
|
|
|
const site = await CoreSites.getSite(siteId);
|
2020-10-07 10:53:19 +02:00
|
|
|
|
2020-10-08 16:33:10 +02:00
|
|
|
file.fileurl = await site.checkAndFixPluginfileURL(file.fileurl);
|
2020-10-07 10:53:19 +02:00
|
|
|
|
2020-10-08 16:33:10 +02:00
|
|
|
return file;
|
2020-10-07 10:53:19 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Convenience function to get component files.
|
|
|
|
*
|
|
|
|
* @param db Site's DB.
|
|
|
|
* @param component The component to get.
|
|
|
|
* @param componentId An ID to use in conjunction with the component.
|
|
|
|
* @return Promise resolved with the files.
|
|
|
|
*/
|
2020-10-21 16:32:27 +02:00
|
|
|
protected async getComponentFiles(
|
|
|
|
db: SQLiteDB,
|
|
|
|
component: string,
|
|
|
|
componentId?: string | number,
|
|
|
|
): Promise<CoreFilepoolLinksRecord[]> {
|
2020-10-07 10:53:19 +02:00
|
|
|
const conditions = {
|
|
|
|
component,
|
|
|
|
componentId: this.fixComponentId(componentId),
|
|
|
|
};
|
|
|
|
|
2020-10-28 14:25:18 +01:00
|
|
|
const items = await db.getRecords<CoreFilepoolLinksRecord>(LINKS_TABLE_NAME, conditions);
|
2020-10-08 16:33:10 +02:00
|
|
|
items.forEach((item) => {
|
|
|
|
item.componentId = this.fixComponentId(item.componentId);
|
2020-10-07 10:53:19 +02:00
|
|
|
});
|
2020-10-08 16:33:10 +02:00
|
|
|
|
|
|
|
return items;
|
2020-10-07 10:53:19 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Returns the local URL of a directory.
|
|
|
|
*
|
|
|
|
* @param siteId The site ID.
|
|
|
|
* @param fileUrl The file URL.
|
|
|
|
* @return Resolved with the URL. Rejected otherwise.
|
|
|
|
*/
|
2020-10-08 16:33:10 +02:00
|
|
|
async getDirectoryUrlByUrl(siteId: string, fileUrl: string): Promise<string> {
|
2021-03-02 11:41:04 +01:00
|
|
|
if (!CoreFile.isAvailable()) {
|
2020-10-14 16:38:24 +02:00
|
|
|
throw new CoreError('File system cannot be used.');
|
2020-10-07 10:53:19 +02:00
|
|
|
}
|
|
|
|
|
2020-10-14 16:38:24 +02:00
|
|
|
const file = await this.fixPluginfileURL(siteId, fileUrl);
|
|
|
|
const fileId = this.getFileIdByUrl(file.fileurl);
|
|
|
|
const filePath = await this.getFilePath(siteId, fileId, '');
|
2021-03-02 11:41:04 +01:00
|
|
|
const dirEntry = await CoreFile.getDir(filePath);
|
2020-10-14 16:38:24 +02:00
|
|
|
|
|
|
|
return dirEntry.toURL();
|
2020-10-07 10:53:19 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Get the ID of a file download. Used to keep track of filePromises.
|
|
|
|
*
|
|
|
|
* @param fileUrl The file URL.
|
|
|
|
* @param filePath The file destination path.
|
|
|
|
* @return File download ID.
|
|
|
|
*/
|
|
|
|
protected getFileDownloadId(fileUrl: string, filePath: string): string {
|
|
|
|
return <string> Md5.hashAsciiStr(fileUrl + '###' + filePath);
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Get the name of the event used to notify download events (CoreEventsProvider).
|
|
|
|
*
|
|
|
|
* @param siteId The site ID.
|
|
|
|
* @param fileId The file ID.
|
|
|
|
* @return Event name.
|
|
|
|
*/
|
|
|
|
protected getFileEventName(siteId: string, fileId: string): string {
|
|
|
|
return 'CoreFilepoolFile:' + siteId + ':' + fileId;
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Get the name of the event used to notify download events (CoreEventsProvider).
|
|
|
|
*
|
|
|
|
* @param siteId The site ID.
|
|
|
|
* @param fileUrl The absolute URL to the file.
|
|
|
|
* @return Promise resolved with event name.
|
|
|
|
*/
|
|
|
|
getFileEventNameByUrl(siteId: string, fileUrl: string): Promise<string> {
|
|
|
|
return this.fixPluginfileURL(siteId, fileUrl).then((file) => {
|
|
|
|
const fileId = this.getFileIdByUrl(file.fileurl);
|
|
|
|
|
|
|
|
return this.getFileEventName(siteId, fileId);
|
|
|
|
});
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Creates a unique ID based on a URL.
|
|
|
|
*
|
|
|
|
* This has a minimal handling of pluginfiles in order to generate a clean file ID which will not change if
|
|
|
|
* pointing to the same pluginfile URL even if the token or extra attributes have changed.
|
|
|
|
*
|
|
|
|
* @param fileUrl The absolute URL to the file.
|
|
|
|
* @return The file ID.
|
|
|
|
*/
|
|
|
|
protected getFileIdByUrl(fileUrl: string): string {
|
|
|
|
let url = fileUrl;
|
|
|
|
|
|
|
|
// If site supports it, since 3.8 we use tokenpluginfile instead of pluginfile.
|
|
|
|
// For compatibility with files already downloaded, we need to use pluginfile to calculate the file ID.
|
2020-10-08 16:33:10 +02:00
|
|
|
url = url.replace(/\/tokenpluginfile\.php\/[^/]+\//, '/webservice/pluginfile.php/');
|
2020-10-07 10:53:19 +02:00
|
|
|
|
|
|
|
// Remove the revision number from the URL so updates on the file aren't detected as a different file.
|
|
|
|
url = this.removeRevisionFromUrl(url);
|
|
|
|
|
|
|
|
// Decode URL.
|
2021-03-02 11:41:04 +01:00
|
|
|
url = CoreTextUtils.decodeHTML(CoreTextUtils.decodeURIComponent(url));
|
2020-10-07 10:53:19 +02:00
|
|
|
|
|
|
|
if (url.indexOf('/webservice/pluginfile') !== -1) {
|
|
|
|
// Remove attributes that do not matter.
|
|
|
|
this.urlAttributes.forEach((regex) => {
|
|
|
|
url = url.replace(regex, '');
|
|
|
|
});
|
|
|
|
}
|
|
|
|
|
|
|
|
// Try to guess the filename the target file should have.
|
|
|
|
// We want to keep the original file name so people can easily identify the files after the download.
|
|
|
|
const filename = this.guessFilenameFromUrl(url);
|
|
|
|
|
|
|
|
return this.addHashToFilename(url, filename);
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Get the links of a file.
|
|
|
|
*
|
|
|
|
* @param siteId The site ID.
|
|
|
|
* @param fileId The file ID.
|
|
|
|
* @return Promise resolved with the links.
|
|
|
|
*/
|
2020-10-08 16:33:10 +02:00
|
|
|
protected async getFileLinks(siteId: string, fileId: string): Promise<CoreFilepoolLinksRecord[]> {
|
2021-03-02 11:41:04 +01:00
|
|
|
const db = await CoreSites.getSiteDb(siteId);
|
2020-10-28 14:25:18 +01:00
|
|
|
const items = await db.getRecords<CoreFilepoolLinksRecord>(LINKS_TABLE_NAME, { fileId });
|
2020-10-14 16:38:24 +02:00
|
|
|
|
2020-10-08 16:33:10 +02:00
|
|
|
items.forEach((item) => {
|
|
|
|
item.componentId = this.fixComponentId(item.componentId);
|
2020-10-07 10:53:19 +02:00
|
|
|
});
|
2020-10-08 16:33:10 +02:00
|
|
|
|
|
|
|
return items;
|
2020-10-07 10:53:19 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Get the path to a file. This does not check if the file exists or not.
|
|
|
|
*
|
|
|
|
* @param siteId The site ID.
|
|
|
|
* @param fileId The file ID.
|
|
|
|
* @param extension Previously calculated extension. Empty to not add any. Undefined to calculate it.
|
|
|
|
* @return The path to the file relative to storage root.
|
|
|
|
*/
|
2020-10-08 16:33:10 +02:00
|
|
|
protected async getFilePath(siteId: string, fileId: string, extension?: string): Promise<string> {
|
2020-10-07 10:53:19 +02:00
|
|
|
let path = this.getFilepoolFolderPath(siteId) + '/' + fileId;
|
2020-10-08 16:33:10 +02:00
|
|
|
|
2020-10-07 10:53:19 +02:00
|
|
|
if (typeof extension == 'undefined') {
|
|
|
|
// We need the extension to be able to open files properly.
|
2020-10-08 16:33:10 +02:00
|
|
|
try {
|
|
|
|
const entry = await this.hasFileInPool(siteId, fileId);
|
|
|
|
|
2020-10-07 10:53:19 +02:00
|
|
|
if (entry.extension) {
|
|
|
|
path += '.' + entry.extension;
|
|
|
|
}
|
2020-10-08 16:33:10 +02:00
|
|
|
} catch (error) {
|
2020-10-07 10:53:19 +02:00
|
|
|
// If file not found, use the path without extension.
|
|
|
|
}
|
2020-10-08 16:33:10 +02:00
|
|
|
} else if (extension) {
|
|
|
|
path += '.' + extension;
|
2020-10-07 10:53:19 +02:00
|
|
|
}
|
2020-10-08 16:33:10 +02:00
|
|
|
|
|
|
|
return path;
|
2020-10-07 10:53:19 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Get the path to a file from its URL. This does not check if the file exists or not.
|
|
|
|
*
|
|
|
|
* @param siteId The site ID.
|
|
|
|
* @param fileUrl The file URL.
|
|
|
|
* @return Promise resolved with the path to the file relative to storage root.
|
|
|
|
*/
|
2020-10-08 16:33:10 +02:00
|
|
|
async getFilePathByUrl(siteId: string, fileUrl: string): Promise<string> {
|
|
|
|
const file = await this.fixPluginfileURL(siteId, fileUrl);
|
|
|
|
const fileId = this.getFileIdByUrl(file.fileurl);
|
2020-10-07 10:53:19 +02:00
|
|
|
|
2020-10-08 16:33:10 +02:00
|
|
|
return this.getFilePath(siteId, fileId);
|
2020-10-07 10:53:19 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Get site Filepool Folder Path
|
|
|
|
*
|
|
|
|
* @param siteId The site ID.
|
|
|
|
* @return The root path to the filepool of the site.
|
|
|
|
*/
|
|
|
|
getFilepoolFolderPath(siteId: string): string {
|
2021-03-02 11:41:04 +01:00
|
|
|
return CoreFile.getSiteFolder(siteId) + '/' + CoreFilepoolProvider.FOLDER;
|
2020-10-07 10:53:19 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Get all the matching files from a component. Returns objects containing properties like path, extension and url.
|
|
|
|
*
|
|
|
|
* @param siteId The site ID.
|
|
|
|
* @param component The component to get.
|
|
|
|
* @param componentId An ID to use in conjunction with the component.
|
|
|
|
* @return Promise resolved with the files on success.
|
|
|
|
*/
|
2020-10-08 16:33:10 +02:00
|
|
|
async getFilesByComponent(siteId: string, component: string, componentId?: string | number): Promise<CoreFilepoolFileEntry[]> {
|
2021-03-02 11:41:04 +01:00
|
|
|
const db = await CoreSites.getSiteDb(siteId);
|
2020-10-08 16:33:10 +02:00
|
|
|
const items = await this.getComponentFiles(db, component, componentId);
|
2020-10-21 16:32:27 +02:00
|
|
|
const files: CoreFilepoolFileEntry[] = [];
|
|
|
|
|
|
|
|
await Promise.all(items.map(async (item) => {
|
|
|
|
try {
|
|
|
|
const fileEntry = await db.getRecord<CoreFilepoolFileEntry>(
|
2020-10-28 14:25:18 +01:00
|
|
|
FILES_TABLE_NAME,
|
2020-10-21 16:32:27 +02:00
|
|
|
{ fileId: item.fileId },
|
|
|
|
);
|
2020-10-07 10:53:19 +02:00
|
|
|
|
2020-10-08 16:33:10 +02:00
|
|
|
if (!fileEntry) {
|
|
|
|
return;
|
|
|
|
}
|
2020-10-07 10:53:19 +02:00
|
|
|
|
2020-10-21 16:32:27 +02:00
|
|
|
files.push(fileEntry);
|
|
|
|
} catch (error) {
|
|
|
|
// File not found, ignore error.
|
|
|
|
}
|
|
|
|
}));
|
2020-10-07 10:53:19 +02:00
|
|
|
|
2020-10-08 16:33:10 +02:00
|
|
|
return files;
|
2020-10-07 10:53:19 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Get the size of all the files from a component.
|
|
|
|
*
|
|
|
|
* @param siteId The site ID.
|
|
|
|
* @param component The component to get.
|
|
|
|
* @param componentId An ID to use in conjunction with the component.
|
|
|
|
* @return Promise resolved with the size on success.
|
|
|
|
*/
|
2020-10-21 16:32:27 +02:00
|
|
|
async getFilesSizeByComponent(siteId: string, component: string, componentId?: string | number): Promise<number> {
|
|
|
|
const files = await this.getFilesByComponent(siteId, component, componentId);
|
2020-10-07 10:53:19 +02:00
|
|
|
|
2020-10-21 16:32:27 +02:00
|
|
|
let size = 0;
|
|
|
|
|
|
|
|
await Promise.all(files.map(async (file) => {
|
|
|
|
try {
|
2021-03-02 11:41:04 +01:00
|
|
|
const fileSize = await CoreFile.getFileSize(file.path);
|
2020-10-21 16:32:27 +02:00
|
|
|
|
|
|
|
size += fileSize;
|
|
|
|
} catch (error) {
|
|
|
|
// Ignore failures, maybe some file was deleted.
|
|
|
|
}
|
|
|
|
}));
|
|
|
|
|
|
|
|
return size;
|
2020-10-07 10:53:19 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Returns the file state: mmCoreDownloaded, mmCoreDownloading, mmCoreNotDownloaded or mmCoreOutdated.
|
|
|
|
*
|
|
|
|
* @param siteId The site ID.
|
|
|
|
* @param fileUrl File URL.
|
|
|
|
* @param timemodified The time this file was modified.
|
|
|
|
* @param filePath Filepath to download the file to. If defined, no extension will be added.
|
|
|
|
* @param revision File revision. If not defined, it will be calculated using the URL.
|
|
|
|
* @return Promise resolved with the file state.
|
|
|
|
*/
|
2020-10-21 16:32:27 +02:00
|
|
|
async getFileStateByUrl(
|
|
|
|
siteId: string,
|
|
|
|
fileUrl: string,
|
|
|
|
timemodified: number = 0,
|
|
|
|
filePath?: string,
|
|
|
|
revision?: number,
|
|
|
|
): Promise<string> {
|
2020-10-08 16:33:10 +02:00
|
|
|
let file: CoreWSExternalFile;
|
2020-10-07 10:53:19 +02:00
|
|
|
|
|
|
|
try {
|
|
|
|
file = await this.fixPluginfileURL(siteId, fileUrl, timemodified);
|
|
|
|
} catch (e) {
|
|
|
|
return CoreConstants.NOT_DOWNLOADABLE;
|
|
|
|
}
|
|
|
|
|
|
|
|
fileUrl = file.fileurl;
|
|
|
|
timemodified = file.timemodified || timemodified;
|
|
|
|
revision = revision || this.getRevisionFromUrl(fileUrl);
|
|
|
|
const fileId = this.getFileIdByUrl(fileUrl);
|
|
|
|
|
|
|
|
try {
|
|
|
|
// Check if the file is in queue (waiting to be downloaded).
|
|
|
|
await this.hasFileInQueue(siteId, fileId);
|
|
|
|
|
|
|
|
return CoreConstants.DOWNLOADING;
|
|
|
|
} catch (e) {
|
|
|
|
// Check if the file is being downloaded right now.
|
2021-03-02 11:41:04 +01:00
|
|
|
const extension = CoreMimetypeUtils.guessExtensionFromUrl(fileUrl);
|
2020-10-07 10:53:19 +02:00
|
|
|
filePath = filePath || (await this.getFilePath(siteId, fileId, extension));
|
|
|
|
|
|
|
|
const downloadId = this.getFileDownloadId(fileUrl, filePath);
|
|
|
|
|
|
|
|
if (this.filePromises[siteId] && this.filePromises[siteId][downloadId]) {
|
|
|
|
return CoreConstants.DOWNLOADING;
|
|
|
|
}
|
|
|
|
|
|
|
|
try {
|
|
|
|
// File is not being downloaded. Check if it's downloaded and if it's outdated.
|
|
|
|
const entry = await this.hasFileInPool(siteId, fileId);
|
|
|
|
|
|
|
|
if (this.isFileOutdated(entry, revision, timemodified)) {
|
|
|
|
return CoreConstants.OUTDATED;
|
|
|
|
}
|
|
|
|
|
|
|
|
return CoreConstants.DOWNLOADED;
|
|
|
|
} catch (e) {
|
|
|
|
return CoreConstants.NOT_DOWNLOADED;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Returns an absolute URL to access the file URL.
|
|
|
|
*
|
|
|
|
* @param siteId The site ID.
|
|
|
|
* @param fileUrl The absolute URL to the file.
|
|
|
|
* @param mode The type of URL to return. Accepts 'url' or 'src'.
|
|
|
|
* @param component The component to link the file to.
|
|
|
|
* @param componentId An ID to use in conjunction with the component.
|
|
|
|
* @param timemodified The time this file was modified.
|
|
|
|
* @param checkSize True if we shouldn't download files if their size is big, false otherwise.
|
2020-10-14 16:38:24 +02:00
|
|
|
* @param downloadUnknown True to download file in WiFi if their size is unknown, false otherwise.
|
2020-10-07 10:53:19 +02:00
|
|
|
* Ignored if checkSize=false.
|
|
|
|
* @param options Extra options (isexternalfile, repositorytype).
|
|
|
|
* @param revision File revision. If not defined, it will be calculated using the URL.
|
|
|
|
* @return Resolved with the URL to use.
|
|
|
|
* @description
|
|
|
|
* This will return a URL pointing to the content of the requested URL.
|
|
|
|
*
|
|
|
|
* This handles the queue and validity of the file. If there is a local file and it's valid, return the local URL.
|
|
|
|
* If the file isn't downloaded or it's outdated, return the online URL and add it to the queue to be downloaded later.
|
|
|
|
*/
|
2020-10-21 16:32:27 +02:00
|
|
|
protected async getFileUrlByUrl(
|
|
|
|
siteId: string,
|
|
|
|
fileUrl: string,
|
|
|
|
component?: string,
|
|
|
|
componentId?: string | number,
|
|
|
|
mode: string = 'url',
|
|
|
|
timemodified: number = 0,
|
|
|
|
checkSize: boolean = true,
|
|
|
|
downloadUnknown?: boolean,
|
|
|
|
options: CoreFilepoolFileOptions = {},
|
|
|
|
revision?: number,
|
|
|
|
): Promise<string> {
|
2020-10-08 16:33:10 +02:00
|
|
|
const addToQueue = (fileUrl: string): void => {
|
|
|
|
// Add the file to queue if needed and ignore errors.
|
2021-03-02 11:41:04 +01:00
|
|
|
CoreUtils.ignoreErrors(this.addToQueueIfNeeded(
|
2020-10-21 16:32:27 +02:00
|
|
|
siteId,
|
|
|
|
fileUrl,
|
|
|
|
component,
|
|
|
|
componentId,
|
|
|
|
timemodified,
|
|
|
|
checkSize,
|
|
|
|
downloadUnknown,
|
|
|
|
options,
|
|
|
|
revision,
|
|
|
|
));
|
2020-10-08 16:33:10 +02:00
|
|
|
};
|
2020-10-07 10:53:19 +02:00
|
|
|
|
2020-10-08 16:33:10 +02:00
|
|
|
const file = await this.fixPluginfileURL(siteId, fileUrl, timemodified);
|
2020-10-21 16:32:27 +02:00
|
|
|
|
2020-10-08 16:33:10 +02:00
|
|
|
fileUrl = file.fileurl;
|
|
|
|
timemodified = file.timemodified || timemodified;
|
|
|
|
revision = revision || this.getRevisionFromUrl(fileUrl);
|
|
|
|
const fileId = this.getFileIdByUrl(fileUrl);
|
|
|
|
|
2020-10-21 16:32:27 +02:00
|
|
|
try {
|
|
|
|
const entry = await this.hasFileInPool(siteId, fileId);
|
2020-10-07 10:53:19 +02:00
|
|
|
|
2020-10-21 16:32:27 +02:00
|
|
|
if (typeof entry === 'undefined') {
|
|
|
|
throw new CoreError('File not downloaded.');
|
2020-10-08 16:33:10 +02:00
|
|
|
}
|
2020-10-07 10:53:19 +02:00
|
|
|
|
2021-03-02 11:41:04 +01:00
|
|
|
if (this.isFileOutdated(entry, revision, timemodified) && CoreApp.isOnline()) {
|
2020-10-21 16:32:27 +02:00
|
|
|
throw new CoreError('File is outdated');
|
2020-10-08 16:33:10 +02:00
|
|
|
}
|
2020-10-21 16:32:27 +02:00
|
|
|
} catch (error) {
|
|
|
|
// The file is not downloaded or it's outdated. Add to queue and return the fixed URL.
|
|
|
|
addToQueue(fileUrl);
|
2020-10-07 10:53:19 +02:00
|
|
|
|
2020-10-21 16:32:27 +02:00
|
|
|
return fileUrl;
|
|
|
|
}
|
2020-10-07 10:53:19 +02:00
|
|
|
|
2020-10-21 16:32:27 +02:00
|
|
|
try {
|
|
|
|
// We found the file entry, now look for the file on disk.
|
|
|
|
if (mode === 'src') {
|
|
|
|
return await this.getInternalSrcById(siteId, fileId);
|
|
|
|
} else {
|
|
|
|
return await this.getInternalUrlById(siteId, fileId);
|
2020-10-08 16:33:10 +02:00
|
|
|
}
|
2020-10-21 16:32:27 +02:00
|
|
|
} catch (error) {
|
|
|
|
// The file is not on disk.
|
|
|
|
// We could not retrieve the file, delete the entries associated with that ID.
|
|
|
|
this.logger.debug('File ' + fileId + ' not found on disk');
|
|
|
|
this.removeFileById(siteId, fileId);
|
2020-10-08 16:33:10 +02:00
|
|
|
addToQueue(fileUrl);
|
2020-10-07 10:53:19 +02:00
|
|
|
|
2020-10-08 16:33:10 +02:00
|
|
|
return fileUrl;
|
2020-10-21 16:32:27 +02:00
|
|
|
}
|
2020-10-07 10:53:19 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Returns the internal SRC of a file.
|
|
|
|
*
|
|
|
|
* The returned URL from this method is typically used with IMG tags.
|
|
|
|
*
|
|
|
|
* @param siteId The site ID.
|
|
|
|
* @param fileId The file ID.
|
|
|
|
* @return Resolved with the internal URL. Rejected otherwise.
|
|
|
|
*/
|
2020-10-08 16:33:10 +02:00
|
|
|
protected async getInternalSrcById(siteId: string, fileId: string): Promise<string> {
|
2021-03-02 11:41:04 +01:00
|
|
|
if (!CoreFile.isAvailable()) {
|
2020-10-14 16:38:24 +02:00
|
|
|
throw new CoreError('File system cannot be used.');
|
2020-10-07 10:53:19 +02:00
|
|
|
}
|
|
|
|
|
2020-10-14 16:38:24 +02:00
|
|
|
const path = await this.getFilePath(siteId, fileId);
|
2021-03-02 11:41:04 +01:00
|
|
|
const fileEntry = await CoreFile.getFile(path);
|
2020-10-14 16:38:24 +02:00
|
|
|
|
2021-03-02 11:41:04 +01:00
|
|
|
return CoreFile.convertFileSrc(fileEntry.toURL());
|
2020-10-07 10:53:19 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Returns the local URL of a file.
|
|
|
|
*
|
|
|
|
* @param siteId The site ID.
|
|
|
|
* @param fileId The file ID.
|
|
|
|
* @return Resolved with the URL. Rejected otherwise.
|
|
|
|
*/
|
2020-10-08 16:33:10 +02:00
|
|
|
protected async getInternalUrlById(siteId: string, fileId: string): Promise<string> {
|
2021-03-02 11:41:04 +01:00
|
|
|
if (!CoreFile.isAvailable()) {
|
2020-10-14 16:38:24 +02:00
|
|
|
throw new CoreError('File system cannot be used.');
|
2020-10-07 10:53:19 +02:00
|
|
|
}
|
|
|
|
|
2020-10-14 16:38:24 +02:00
|
|
|
const path = await this.getFilePath(siteId, fileId);
|
2021-03-02 11:41:04 +01:00
|
|
|
const fileEntry = await CoreFile.getFile(path);
|
2020-10-14 16:38:24 +02:00
|
|
|
|
2020-10-29 12:39:15 +01:00
|
|
|
// This URL is usually used to launch files or put them in HTML.
|
|
|
|
return fileEntry.toURL();
|
2020-10-07 10:53:19 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Returns the local URL of a file.
|
|
|
|
*
|
|
|
|
* @param filePath The file path.
|
|
|
|
* @return Resolved with the URL.
|
|
|
|
*/
|
2020-10-08 16:33:10 +02:00
|
|
|
protected async getInternalUrlByPath(filePath: string): Promise<string> {
|
2021-03-02 11:41:04 +01:00
|
|
|
if (!CoreFile.isAvailable()) {
|
2020-10-14 16:38:24 +02:00
|
|
|
throw new CoreError('File system cannot be used.');
|
2020-10-07 10:53:19 +02:00
|
|
|
}
|
|
|
|
|
2021-03-02 11:41:04 +01:00
|
|
|
const fileEntry = await CoreFile.getFile(filePath);
|
2020-10-14 16:38:24 +02:00
|
|
|
|
|
|
|
return fileEntry.toURL();
|
2020-10-07 10:53:19 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Returns the local URL of a file.
|
|
|
|
*
|
|
|
|
* @param siteId The site ID.
|
|
|
|
* @param fileUrl The file URL.
|
|
|
|
* @return Resolved with the URL. Rejected otherwise.
|
|
|
|
*/
|
2020-10-08 16:33:10 +02:00
|
|
|
async getInternalUrlByUrl(siteId: string, fileUrl: string): Promise<string> {
|
2021-03-02 11:41:04 +01:00
|
|
|
if (!CoreFile.isAvailable()) {
|
2020-10-14 16:38:24 +02:00
|
|
|
throw new CoreError('File system cannot be used.');
|
2020-10-07 10:53:19 +02:00
|
|
|
}
|
|
|
|
|
2020-10-14 16:38:24 +02:00
|
|
|
const file = await this.fixPluginfileURL(siteId, fileUrl);
|
|
|
|
const fileId = this.getFileIdByUrl(file.fileurl);
|
|
|
|
|
|
|
|
return this.getInternalUrlById(siteId, fileId);
|
2020-10-07 10:53:19 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Get the data stored for a package.
|
|
|
|
*
|
|
|
|
* @param siteId Site ID.
|
|
|
|
* @param component Package's component.
|
|
|
|
* @param componentId An ID to use in conjunction with the component.
|
|
|
|
* @return Promise resolved with the data.
|
|
|
|
*/
|
2020-10-08 16:33:10 +02:00
|
|
|
async getPackageData(siteId: string, component: string, componentId?: string | number): Promise<CoreFilepoolPackageEntry> {
|
2020-10-07 10:53:19 +02:00
|
|
|
componentId = this.fixComponentId(componentId);
|
|
|
|
|
2021-03-02 11:41:04 +01:00
|
|
|
const site = await CoreSites.getSite(siteId);
|
2020-10-08 16:33:10 +02:00
|
|
|
const packageId = this.getPackageId(component, componentId);
|
2020-10-07 10:53:19 +02:00
|
|
|
|
2020-10-28 14:25:18 +01:00
|
|
|
return site.getDb().getRecord(PACKAGES_TABLE_NAME, { id: packageId });
|
2020-10-07 10:53:19 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Creates the name for a package directory (hash).
|
|
|
|
*
|
|
|
|
* @param url An URL to identify the package.
|
|
|
|
* @return The directory name.
|
|
|
|
*/
|
|
|
|
protected getPackageDirNameByUrl(url: string): string {
|
|
|
|
let extension = '';
|
|
|
|
|
|
|
|
url = this.removeRevisionFromUrl(url);
|
|
|
|
|
|
|
|
if (url.indexOf('/webservice/pluginfile') !== -1) {
|
|
|
|
// Remove attributes that do not matter.
|
|
|
|
this.urlAttributes.forEach((regex) => {
|
|
|
|
url = url.replace(regex, '');
|
|
|
|
});
|
|
|
|
|
|
|
|
// Guess the extension of the URL. This is for backwards compatibility.
|
2021-03-02 11:41:04 +01:00
|
|
|
const candidate = CoreMimetypeUtils.guessExtensionFromUrl(url);
|
2020-10-07 10:53:19 +02:00
|
|
|
if (candidate && candidate !== 'php') {
|
|
|
|
extension = '.' + candidate;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
return Md5.hashAsciiStr('url:' + url) + extension;
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Get the path to a directory to store a package files. This does not check if the file exists or not.
|
|
|
|
*
|
|
|
|
* @param siteId The site ID.
|
|
|
|
* @param url An URL to identify the package.
|
|
|
|
* @return Promise resolved with the path of the package.
|
|
|
|
*/
|
|
|
|
getPackageDirPathByUrl(siteId: string, url: string): Promise<string> {
|
|
|
|
return this.fixPluginfileURL(siteId, url).then((file) => {
|
|
|
|
const dirName = this.getPackageDirNameByUrl(file.fileurl);
|
|
|
|
|
|
|
|
return this.getFilePath(siteId, dirName, '');
|
|
|
|
});
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Returns the local URL of a package directory.
|
|
|
|
*
|
|
|
|
* @param siteId The site ID.
|
|
|
|
* @param url An URL to identify the package.
|
|
|
|
* @return Resolved with the URL.
|
|
|
|
*/
|
2020-10-08 16:33:10 +02:00
|
|
|
async getPackageDirUrlByUrl(siteId: string, url: string): Promise<string> {
|
2021-03-02 11:41:04 +01:00
|
|
|
if (!CoreFile.isAvailable()) {
|
2020-10-14 16:38:24 +02:00
|
|
|
throw new CoreError('File system cannot be used.');
|
2020-10-07 10:53:19 +02:00
|
|
|
}
|
|
|
|
|
2020-10-14 16:38:24 +02:00
|
|
|
const file = await this.fixPluginfileURL(siteId, url);
|
|
|
|
const dirName = this.getPackageDirNameByUrl(file.fileurl);
|
|
|
|
const dirPath = await this.getFilePath(siteId, dirName, '');
|
2021-03-02 11:41:04 +01:00
|
|
|
const dirEntry = await CoreFile.getDir(dirPath);
|
2020-10-14 16:38:24 +02:00
|
|
|
|
|
|
|
return dirEntry.toURL();
|
2020-10-07 10:53:19 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Get a download promise. If the promise is not set, return undefined.
|
|
|
|
*
|
|
|
|
* @param siteId Site ID.
|
|
|
|
* @param component The component of the package.
|
|
|
|
* @param componentId An ID to use in conjunction with the component.
|
|
|
|
* @return Download promise or undefined.
|
|
|
|
*/
|
2020-10-21 16:32:27 +02:00
|
|
|
getPackageDownloadPromise(siteId: string, component: string, componentId?: string | number): Promise<void> | undefined {
|
2020-10-07 10:53:19 +02:00
|
|
|
const packageId = this.getPackageId(component, componentId);
|
|
|
|
if (this.packagesPromises[siteId] && this.packagesPromises[siteId][packageId]) {
|
|
|
|
return this.packagesPromises[siteId][packageId];
|
|
|
|
}
|
|
|
|
}
|
2020-10-08 16:33:10 +02:00
|
|
|
|
2020-10-07 10:53:19 +02:00
|
|
|
/**
|
|
|
|
* Get a package extra data.
|
|
|
|
*
|
|
|
|
* @param siteId Site ID.
|
|
|
|
* @param component Package's component.
|
|
|
|
* @param componentId An ID to use in conjunction with the component.
|
|
|
|
* @return Promise resolved with the extra data.
|
|
|
|
*/
|
2020-10-21 16:32:27 +02:00
|
|
|
getPackageExtra(siteId: string, component: string, componentId?: string | number): Promise<string | undefined> {
|
2020-10-08 16:33:10 +02:00
|
|
|
return this.getPackageData(siteId, component, componentId).then((entry) => entry.extra);
|
2020-10-07 10:53:19 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Get the ID of a package.
|
|
|
|
*
|
|
|
|
* @param component Package's component.
|
|
|
|
* @param componentId An ID to use in conjunction with the component.
|
|
|
|
* @return Package ID.
|
|
|
|
*/
|
|
|
|
getPackageId(component: string, componentId?: string | number): string {
|
|
|
|
return <string> Md5.hashAsciiStr(component + '#' + this.fixComponentId(componentId));
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Get a package previous status.
|
|
|
|
*
|
|
|
|
* @param siteId Site ID.
|
|
|
|
* @param component Package's component.
|
|
|
|
* @param componentId An ID to use in conjunction with the component.
|
|
|
|
* @return Promise resolved with the status.
|
|
|
|
*/
|
2020-10-08 16:33:10 +02:00
|
|
|
async getPackagePreviousStatus(siteId: string, component: string, componentId?: string | number): Promise<string> {
|
|
|
|
try {
|
|
|
|
const entry = await this.getPackageData(siteId, component, componentId);
|
|
|
|
|
2020-10-07 10:53:19 +02:00
|
|
|
return entry.previous || CoreConstants.NOT_DOWNLOADED;
|
2020-10-08 16:33:10 +02:00
|
|
|
} catch (error) {
|
2020-10-07 10:53:19 +02:00
|
|
|
return CoreConstants.NOT_DOWNLOADED;
|
2020-10-08 16:33:10 +02:00
|
|
|
}
|
2020-10-07 10:53:19 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Get a package status.
|
|
|
|
*
|
|
|
|
* @param siteId Site ID.
|
|
|
|
* @param component Package's component.
|
|
|
|
* @param componentId An ID to use in conjunction with the component.
|
|
|
|
* @return Promise resolved with the status.
|
|
|
|
*/
|
2020-10-08 16:33:10 +02:00
|
|
|
async getPackageStatus(siteId: string, component: string, componentId?: string | number): Promise<string> {
|
|
|
|
try {
|
|
|
|
const entry = await this.getPackageData(siteId, component, componentId);
|
|
|
|
|
2020-10-07 10:53:19 +02:00
|
|
|
return entry.status || CoreConstants.NOT_DOWNLOADED;
|
2020-10-08 16:33:10 +02:00
|
|
|
} catch (error) {
|
2020-10-07 10:53:19 +02:00
|
|
|
return CoreConstants.NOT_DOWNLOADED;
|
2020-10-08 16:33:10 +02:00
|
|
|
}
|
2020-10-07 10:53:19 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Return the array of arguments of the pluginfile url.
|
|
|
|
*
|
|
|
|
* @param url URL to get the args.
|
|
|
|
* @return The args found, undefined if not a pluginfile.
|
|
|
|
*/
|
2020-10-21 16:32:27 +02:00
|
|
|
protected getPluginFileArgs(url: string): string[] | undefined {
|
2021-03-02 11:41:04 +01:00
|
|
|
if (!CoreUrlUtils.isPluginFileUrl(url)) {
|
2020-10-07 10:53:19 +02:00
|
|
|
// Not pluginfile, return.
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
const relativePath = url.substr(url.indexOf('/pluginfile.php') + 16);
|
|
|
|
const args = relativePath.split('/');
|
|
|
|
|
|
|
|
if (args.length < 3) {
|
|
|
|
// To be a plugin file it should have at least contextId, Component and Filearea.
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
return args;
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Get the deferred object for a file in the queue.
|
|
|
|
*
|
|
|
|
* @param siteId The site ID.
|
|
|
|
* @param fileId The file ID.
|
|
|
|
* @param create True if it should create a new deferred if it doesn't exist.
|
|
|
|
* @param onProgress Function to call on progress.
|
|
|
|
* @return Deferred.
|
|
|
|
*/
|
2020-10-21 16:32:27 +02:00
|
|
|
protected getQueueDeferred(
|
|
|
|
siteId: string,
|
|
|
|
fileId: string,
|
|
|
|
create: boolean = true,
|
|
|
|
onProgress?: CoreFilepoolOnProgressCallback,
|
|
|
|
): CoreFilepoolPromiseDefer | undefined {
|
2020-10-07 10:53:19 +02:00
|
|
|
if (!this.queueDeferreds[siteId]) {
|
|
|
|
if (!create) {
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
this.queueDeferreds[siteId] = {};
|
|
|
|
}
|
|
|
|
if (!this.queueDeferreds[siteId][fileId]) {
|
|
|
|
if (!create) {
|
|
|
|
return;
|
|
|
|
}
|
2021-03-02 11:41:04 +01:00
|
|
|
this.queueDeferreds[siteId][fileId] = CoreUtils.promiseDefer();
|
2020-10-07 10:53:19 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
if (onProgress) {
|
|
|
|
this.queueDeferreds[siteId][fileId].onProgress = onProgress;
|
|
|
|
}
|
|
|
|
|
|
|
|
return this.queueDeferreds[siteId][fileId];
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Get the on progress for a file in the queue.
|
|
|
|
*
|
|
|
|
* @param siteId The site ID.
|
|
|
|
* @param fileId The file ID.
|
|
|
|
* @return On progress function, undefined if not found.
|
|
|
|
*/
|
2020-10-21 16:32:27 +02:00
|
|
|
protected getQueueOnProgress(siteId: string, fileId: string): CoreFilepoolOnProgressCallback | undefined {
|
2020-10-07 10:53:19 +02:00
|
|
|
const deferred = this.getQueueDeferred(siteId, fileId, false);
|
2020-10-21 16:32:27 +02:00
|
|
|
|
|
|
|
return deferred?.onProgress;
|
2020-10-07 10:53:19 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Get the promise for a file in the queue.
|
|
|
|
*
|
|
|
|
* @param siteId The site ID.
|
|
|
|
* @param fileId The file ID.
|
|
|
|
* @param create True if it should create a new promise if it doesn't exist.
|
|
|
|
* @param onProgress Function to call on progress.
|
|
|
|
* @return Promise.
|
|
|
|
*/
|
2020-10-21 16:32:27 +02:00
|
|
|
protected getQueuePromise(
|
|
|
|
siteId: string,
|
|
|
|
fileId: string,
|
|
|
|
create: boolean = true,
|
|
|
|
onProgress?: CoreFilepoolOnProgressCallback,
|
|
|
|
): Promise<void> | undefined {
|
|
|
|
const deferred = this.getQueueDeferred(siteId, fileId, create, onProgress);
|
|
|
|
|
|
|
|
return deferred?.promise;
|
2020-10-07 10:53:19 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Get a revision number from a list of files (highest revision).
|
|
|
|
*
|
|
|
|
* @param files Package files.
|
|
|
|
* @return Highest revision.
|
|
|
|
*/
|
2020-10-08 16:33:10 +02:00
|
|
|
getRevisionFromFileList(files: CoreWSExternalFile[]): number {
|
2020-10-07 10:53:19 +02:00
|
|
|
let revision = 0;
|
|
|
|
|
|
|
|
files.forEach((file) => {
|
2020-10-08 16:33:10 +02:00
|
|
|
if (file.fileurl) {
|
|
|
|
const r = this.getRevisionFromUrl(file.fileurl);
|
2020-10-07 10:53:19 +02:00
|
|
|
if (r > revision) {
|
|
|
|
revision = r;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
});
|
|
|
|
|
|
|
|
return revision;
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Get the revision number from a file URL.
|
|
|
|
*
|
|
|
|
* @param url URL to get the revision number.
|
|
|
|
* @return Revision number.
|
|
|
|
*/
|
|
|
|
protected getRevisionFromUrl(url: string): number {
|
|
|
|
const args = this.getPluginFileArgs(url);
|
|
|
|
if (!args) {
|
|
|
|
// Not a pluginfile, no revision will be found.
|
|
|
|
return 0;
|
|
|
|
}
|
|
|
|
|
2021-03-02 11:41:04 +01:00
|
|
|
const revisionRegex = CorePluginFileDelegate.getComponentRevisionRegExp(args);
|
2020-10-07 10:53:19 +02:00
|
|
|
if (!revisionRegex) {
|
|
|
|
return 0;
|
|
|
|
}
|
|
|
|
|
|
|
|
const matches = url.match(revisionRegex);
|
|
|
|
if (matches && typeof matches[1] != 'undefined') {
|
|
|
|
return parseInt(matches[1], 10);
|
|
|
|
}
|
|
|
|
|
|
|
|
return 0;
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Returns an absolute URL to use in IMG tags.
|
|
|
|
*
|
|
|
|
* @param siteId The site ID.
|
|
|
|
* @param fileUrl The absolute URL to the file.
|
|
|
|
* @param mode The type of URL to return. Accepts 'url' or 'src'.
|
|
|
|
* @param component The component to link the file to.
|
|
|
|
* @param componentId An ID to use in conjunction with the component.
|
|
|
|
* @param timemodified The time this file was modified.
|
|
|
|
* @param checkSize True if we shouldn't download files if their size is big, false otherwise.
|
2020-10-14 16:38:24 +02:00
|
|
|
* @param downloadUnknown True to download file in WiFi if their size is unknown, false otherwise.
|
2020-10-07 10:53:19 +02:00
|
|
|
* Ignored if checkSize=false.
|
|
|
|
* @param options Extra options (isexternalfile, repositorytype).
|
|
|
|
* @param revision File revision. If not defined, it will be calculated using the URL.
|
|
|
|
* @return Resolved with the URL to use.
|
|
|
|
* @description
|
|
|
|
* This will return a URL pointing to the content of the requested URL.
|
|
|
|
* The URL returned is compatible to use with IMG tags.
|
|
|
|
*/
|
2020-10-21 16:32:27 +02:00
|
|
|
getSrcByUrl(
|
|
|
|
siteId: string,
|
|
|
|
fileUrl: string,
|
|
|
|
component?: string,
|
|
|
|
componentId?: string | number,
|
|
|
|
timemodified: number = 0,
|
|
|
|
checkSize: boolean = true,
|
|
|
|
downloadUnknown?: boolean,
|
|
|
|
options: CoreFilepoolFileOptions = {},
|
|
|
|
revision?: number,
|
|
|
|
): Promise<string> {
|
|
|
|
return this.getFileUrlByUrl(
|
|
|
|
siteId,
|
|
|
|
fileUrl,
|
|
|
|
component,
|
|
|
|
componentId,
|
|
|
|
'src',
|
|
|
|
timemodified,
|
|
|
|
checkSize,
|
|
|
|
downloadUnknown,
|
|
|
|
options,
|
|
|
|
revision,
|
|
|
|
);
|
2020-10-07 10:53:19 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Get time modified from a list of files.
|
|
|
|
*
|
|
|
|
* @param files List of files.
|
|
|
|
* @return Time modified.
|
|
|
|
*/
|
2020-10-08 16:33:10 +02:00
|
|
|
getTimemodifiedFromFileList(files: CoreWSExternalFile[]): number {
|
2020-10-07 10:53:19 +02:00
|
|
|
let timemodified = 0;
|
|
|
|
|
|
|
|
files.forEach((file) => {
|
2020-10-21 16:32:27 +02:00
|
|
|
if (file.timemodified && file.timemodified > timemodified) {
|
2020-10-07 10:53:19 +02:00
|
|
|
timemodified = file.timemodified;
|
|
|
|
}
|
|
|
|
});
|
|
|
|
|
|
|
|
return timemodified;
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Returns an absolute URL to access the file.
|
|
|
|
*
|
|
|
|
* @param siteId The site ID.
|
|
|
|
* @param fileUrl The absolute URL to the file.
|
|
|
|
* @param mode The type of URL to return. Accepts 'url' or 'src'.
|
|
|
|
* @param component The component to link the file to.
|
|
|
|
* @param componentId An ID to use in conjunction with the component.
|
|
|
|
* @param timemodified The time this file was modified.
|
|
|
|
* @param checkSize True if we shouldn't download files if their size is big, false otherwise.
|
2020-10-14 16:38:24 +02:00
|
|
|
* @param downloadUnknown True to download file in WiFi if their size is unknown, false otherwise.
|
2020-10-07 10:53:19 +02:00
|
|
|
* Ignored if checkSize=false.
|
|
|
|
* @param options Extra options (isexternalfile, repositorytype).
|
|
|
|
* @param revision File revision. If not defined, it will be calculated using the URL.
|
|
|
|
* @return Resolved with the URL to use.
|
|
|
|
* @description
|
|
|
|
* This will return a URL pointing to the content of the requested URL.
|
|
|
|
* The URL returned is compatible to use with a local browser.
|
|
|
|
*/
|
2020-10-21 16:32:27 +02:00
|
|
|
getUrlByUrl(
|
|
|
|
siteId: string,
|
|
|
|
fileUrl: string,
|
|
|
|
component?: string,
|
|
|
|
componentId?: string | number,
|
|
|
|
timemodified: number = 0,
|
|
|
|
checkSize: boolean = true,
|
|
|
|
downloadUnknown?: boolean,
|
|
|
|
options: CoreFilepoolFileOptions = {},
|
|
|
|
revision?: number,
|
|
|
|
): Promise<string> {
|
|
|
|
return this.getFileUrlByUrl(
|
|
|
|
siteId,
|
|
|
|
fileUrl,
|
|
|
|
component,
|
|
|
|
componentId,
|
|
|
|
'url',
|
|
|
|
timemodified,
|
|
|
|
checkSize,
|
|
|
|
downloadUnknown,
|
|
|
|
options,
|
|
|
|
revision,
|
|
|
|
);
|
2020-10-07 10:53:19 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Guess the filename of a file from its URL. This is very weak and unreliable.
|
|
|
|
*
|
|
|
|
* @param fileUrl The file URL.
|
|
|
|
* @return The filename treated so it doesn't have any special character.
|
|
|
|
*/
|
|
|
|
protected guessFilenameFromUrl(fileUrl: string): string {
|
|
|
|
let filename = '';
|
|
|
|
|
|
|
|
if (fileUrl.indexOf('/webservice/pluginfile') !== -1) {
|
|
|
|
// It's a pluginfile URL. Search for the 'file' param to extract the name.
|
2021-03-02 11:41:04 +01:00
|
|
|
const params = CoreUrlUtils.extractUrlParams(fileUrl);
|
2020-10-07 10:53:19 +02:00
|
|
|
if (params.file) {
|
|
|
|
filename = params.file.substr(params.file.lastIndexOf('/') + 1);
|
|
|
|
} else {
|
|
|
|
// 'file' param not found. Extract what's after the last '/' without params.
|
2021-03-02 11:41:04 +01:00
|
|
|
filename = CoreUrlUtils.getLastFileWithoutParams(fileUrl);
|
2020-10-07 10:53:19 +02:00
|
|
|
}
|
2021-03-02 11:41:04 +01:00
|
|
|
} else if (CoreUrlUtils.isGravatarUrl(fileUrl)) {
|
2020-10-07 10:53:19 +02:00
|
|
|
// Extract gravatar ID.
|
2021-03-02 11:41:04 +01:00
|
|
|
filename = 'gravatar_' + CoreUrlUtils.getLastFileWithoutParams(fileUrl);
|
|
|
|
} else if (CoreUrlUtils.isThemeImageUrl(fileUrl)) {
|
2020-10-07 10:53:19 +02:00
|
|
|
// Extract user ID.
|
2020-10-08 16:33:10 +02:00
|
|
|
const matches = fileUrl.match(/\/core\/([^/]*)\//);
|
2020-10-07 10:53:19 +02:00
|
|
|
if (matches && matches[1]) {
|
|
|
|
filename = matches[1];
|
|
|
|
}
|
|
|
|
// Attach a constant and the image type.
|
2021-03-02 11:41:04 +01:00
|
|
|
filename = 'default_' + filename + '_' + CoreUrlUtils.getLastFileWithoutParams(fileUrl);
|
2020-10-07 10:53:19 +02:00
|
|
|
} else {
|
|
|
|
// Another URL. Just get what's after the last /.
|
2021-03-02 11:41:04 +01:00
|
|
|
filename = CoreUrlUtils.getLastFileWithoutParams(fileUrl);
|
2020-10-07 10:53:19 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
// If there are hashes in the URL, extract them.
|
|
|
|
const index = filename.indexOf('#');
|
2020-10-21 16:32:27 +02:00
|
|
|
let hashes: string[] | undefined;
|
2020-10-07 10:53:19 +02:00
|
|
|
|
|
|
|
if (index != -1) {
|
|
|
|
hashes = filename.split('#');
|
|
|
|
|
|
|
|
// Remove the URL from the array.
|
|
|
|
hashes.shift();
|
|
|
|
|
|
|
|
filename = filename.substr(0, index);
|
|
|
|
}
|
|
|
|
|
|
|
|
// Remove the extension from the filename.
|
2021-03-02 11:41:04 +01:00
|
|
|
filename = CoreMimetypeUtils.removeExtension(filename);
|
2020-10-07 10:53:19 +02:00
|
|
|
|
|
|
|
if (hashes) {
|
|
|
|
// Add hashes to the name.
|
|
|
|
filename += '_' + hashes.join('_');
|
|
|
|
}
|
|
|
|
|
2021-03-02 11:41:04 +01:00
|
|
|
return CoreTextUtils.removeSpecialCharactersForFiles(filename);
|
2020-10-07 10:53:19 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Check if the file is already in the pool. This does not check if the file is on the disk.
|
|
|
|
*
|
|
|
|
* @param siteId The site ID.
|
|
|
|
* @param fileUrl The file URL.
|
|
|
|
* @return Resolved with file object from DB on success, rejected otherwise.
|
|
|
|
*/
|
2020-10-08 16:33:10 +02:00
|
|
|
protected async hasFileInPool(siteId: string, fileId: string): Promise<CoreFilepoolFileEntry> {
|
2021-03-02 11:41:04 +01:00
|
|
|
const db = await CoreSites.getSiteDb(siteId);
|
2020-10-28 14:25:18 +01:00
|
|
|
const entry = await db.getRecord<CoreFilepoolFileEntry>(FILES_TABLE_NAME, { fileId });
|
2020-10-14 16:38:24 +02:00
|
|
|
|
2020-10-08 16:33:10 +02:00
|
|
|
if (typeof entry === 'undefined') {
|
2020-10-14 16:38:24 +02:00
|
|
|
throw new CoreError('File not found in filepool.');
|
2020-10-08 16:33:10 +02:00
|
|
|
}
|
2020-10-07 10:53:19 +02:00
|
|
|
|
2020-10-08 16:33:10 +02:00
|
|
|
return entry;
|
2020-10-07 10:53:19 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Check if the file is in the queue.
|
|
|
|
*
|
|
|
|
* @param siteId The site ID.
|
|
|
|
* @param fileUrl The file URL.
|
|
|
|
* @return Resolved with file object from DB on success, rejected otherwise.
|
|
|
|
*/
|
|
|
|
protected async hasFileInQueue(siteId: string, fileId: string): Promise<CoreFilepoolQueueEntry> {
|
2020-12-01 18:37:24 +01:00
|
|
|
const db = await this.appDB;
|
|
|
|
const entry = await db.getRecord<CoreFilepoolQueueEntry>(QUEUE_TABLE_NAME, { siteId, fileId });
|
2020-10-14 16:38:24 +02:00
|
|
|
|
2020-10-07 10:53:19 +02:00
|
|
|
if (typeof entry === 'undefined') {
|
2020-10-14 16:38:24 +02:00
|
|
|
throw new CoreError('File not found in queue.');
|
2020-10-07 10:53:19 +02:00
|
|
|
}
|
|
|
|
// Convert the links to an object.
|
2021-03-02 11:41:04 +01:00
|
|
|
entry.linksUnserialized = <CoreFilepoolComponentLink[]> CoreTextUtils.parseJSON(entry.links || '[]', []);
|
2020-10-07 10:53:19 +02:00
|
|
|
|
|
|
|
return entry;
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Invalidate all the files in a site.
|
|
|
|
*
|
|
|
|
* @param siteId The site ID.
|
2020-10-14 16:38:24 +02:00
|
|
|
* @param onlyUnknown True to only invalidate files from external repos or without revision/timemodified.
|
2020-10-07 10:53:19 +02:00
|
|
|
* It is advised to set it to true to reduce the performance and data usage of the app.
|
|
|
|
* @return Resolved on success.
|
|
|
|
*/
|
2020-10-14 16:38:24 +02:00
|
|
|
async invalidateAllFiles(siteId: string, onlyUnknown: boolean = true): Promise<void> {
|
2021-03-02 11:41:04 +01:00
|
|
|
const db = await CoreSites.getSiteDb(siteId);
|
2020-10-07 10:53:19 +02:00
|
|
|
|
2020-10-21 16:32:27 +02:00
|
|
|
const where = onlyUnknown ? CoreFilepoolProvider.FILE_UPDATE_UNKNOWN_WHERE_CLAUSE : undefined;
|
2020-10-07 10:53:19 +02:00
|
|
|
|
2020-10-28 14:25:18 +01:00
|
|
|
await db.updateRecordsWhere(FILES_TABLE_NAME, { stale: 1 }, where);
|
2020-10-07 10:53:19 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Invalidate a file by URL.
|
|
|
|
*
|
|
|
|
* @param siteId The site ID.
|
|
|
|
* @param fileUrl The file URL.
|
|
|
|
* @return Resolved on success.
|
|
|
|
* @description
|
|
|
|
* Invalidates a file by marking it stale. It will not be added to the queue automatically, but the next time this file
|
|
|
|
* is requested it will be added to the queue.
|
|
|
|
* You can manully call addToQueueByUrl to add this file to the queue immediately.
|
|
|
|
* Please note that, if a file is stale, the user will be presented the stale file if there is no network access.
|
|
|
|
*/
|
2020-10-08 16:33:10 +02:00
|
|
|
async invalidateFileByUrl(siteId: string, fileUrl: string): Promise<void> {
|
|
|
|
const file = await this.fixPluginfileURL(siteId, fileUrl);
|
|
|
|
const fileId = this.getFileIdByUrl(file.fileurl);
|
2020-10-07 10:53:19 +02:00
|
|
|
|
2021-03-02 11:41:04 +01:00
|
|
|
const db = await CoreSites.getSiteDb(siteId);
|
2020-10-08 16:33:10 +02:00
|
|
|
|
2020-10-28 14:25:18 +01:00
|
|
|
await db.updateRecords(FILES_TABLE_NAME, { stale: 1 }, { fileId });
|
2020-10-07 10:53:19 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Invalidate all the matching files from a component.
|
|
|
|
*
|
|
|
|
* @param siteId The site ID.
|
|
|
|
* @param component The component to invalidate.
|
|
|
|
* @param componentId An ID to use in conjunction with the component.
|
2020-10-14 16:38:24 +02:00
|
|
|
* @param onlyUnknown True to only invalidate files from external repos or without revision/timemodified.
|
|
|
|
* It is advised to set it to true to reduce the performance and data usage of the app.
|
2020-10-07 10:53:19 +02:00
|
|
|
* @return Resolved when done.
|
|
|
|
*/
|
2020-10-21 16:32:27 +02:00
|
|
|
async invalidateFilesByComponent(
|
2021-03-18 17:17:47 +01:00
|
|
|
siteId: string | undefined,
|
2020-10-21 16:32:27 +02:00
|
|
|
component: string,
|
|
|
|
componentId?: string | number,
|
|
|
|
onlyUnknown: boolean = true,
|
|
|
|
): Promise<void> {
|
2021-03-02 11:41:04 +01:00
|
|
|
const db = await CoreSites.getSiteDb(siteId);
|
2020-10-07 10:53:19 +02:00
|
|
|
|
|
|
|
const items = await this.getComponentFiles(db, component, componentId);
|
|
|
|
|
|
|
|
if (!items.length) {
|
|
|
|
// Nothing to invalidate.
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
const fileIds = items.map((item) => item.fileId);
|
2021-02-08 14:29:11 +01:00
|
|
|
|
2020-10-07 10:53:19 +02:00
|
|
|
const whereAndParams = db.getInOrEqual(fileIds);
|
|
|
|
|
2021-02-08 14:29:11 +01:00
|
|
|
whereAndParams.sql = 'fileId ' + whereAndParams.sql;
|
2020-10-07 10:53:19 +02:00
|
|
|
|
2020-10-14 16:38:24 +02:00
|
|
|
if (onlyUnknown) {
|
2021-02-08 14:29:11 +01:00
|
|
|
whereAndParams.sql += ' AND (' + CoreFilepoolProvider.FILE_UPDATE_UNKNOWN_WHERE_CLAUSE + ')';
|
2020-10-07 10:53:19 +02:00
|
|
|
}
|
|
|
|
|
2021-02-08 14:29:11 +01:00
|
|
|
await db.updateRecordsWhere(FILES_TABLE_NAME, { stale: 1 }, whereAndParams.sql, whereAndParams.params);
|
2020-10-07 10:53:19 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Whether a file action indicates a file was downloaded or deleted.
|
|
|
|
*
|
|
|
|
* @param data Event data.
|
|
|
|
* @return Whether downloaded or deleted.
|
|
|
|
*/
|
|
|
|
isFileEventDownloadedOrDeleted(data: CoreFilepoolFileEventData): boolean {
|
|
|
|
return (data.action == CoreFilepoolFileActions.DOWNLOAD && data.success == true) ||
|
|
|
|
data.action == CoreFilepoolFileActions.DELETED;
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Check whether a file is downloadable.
|
|
|
|
*
|
|
|
|
* @param siteId The site ID.
|
|
|
|
* @param fileUrl File URL.
|
|
|
|
* @param timemodified The time this file was modified.
|
|
|
|
* @param filePath Filepath to download the file to. If defined, no extension will be added.
|
|
|
|
* @param revision File revision. If not defined, it will be calculated using the URL.
|
|
|
|
* @return Promise resolved with a boolean: whether a file is downloadable.
|
|
|
|
*/
|
2020-10-21 16:32:27 +02:00
|
|
|
async isFileDownloadable(
|
|
|
|
siteId: string,
|
|
|
|
fileUrl: string,
|
|
|
|
timemodified: number = 0,
|
|
|
|
filePath?: string,
|
|
|
|
revision?: number,
|
|
|
|
): Promise<boolean> {
|
2020-10-07 10:53:19 +02:00
|
|
|
const state = await this.getFileStateByUrl(siteId, fileUrl, timemodified, filePath, revision);
|
|
|
|
|
|
|
|
return state != CoreConstants.NOT_DOWNLOADABLE;
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Check if a file is downloading.
|
|
|
|
*
|
|
|
|
* @param siteId The site ID.
|
|
|
|
* @param fileUrl File URL.
|
|
|
|
* @param Promise resolved if file is downloading, rejected otherwise.
|
|
|
|
*/
|
2020-10-08 16:33:10 +02:00
|
|
|
async isFileDownloadingByUrl(siteId: string, fileUrl: string): Promise<void> {
|
|
|
|
const file = await this.fixPluginfileURL(siteId, fileUrl);
|
|
|
|
const fileId = this.getFileIdByUrl(file.fileurl);
|
2020-10-07 10:53:19 +02:00
|
|
|
|
2020-10-08 16:33:10 +02:00
|
|
|
await this.hasFileInQueue(siteId, fileId);
|
2020-10-07 10:53:19 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Check if a file is outdated.
|
|
|
|
*
|
|
|
|
* @param entry Filepool entry.
|
|
|
|
* @param revision File revision number.
|
|
|
|
* @param timemodified The time this file was modified.
|
|
|
|
* @param Whether the file is outdated.
|
|
|
|
*/
|
|
|
|
protected isFileOutdated(entry: CoreFilepoolFileEntry, revision?: number, timemodified?: number): boolean {
|
2020-10-21 16:32:27 +02:00
|
|
|
return !!entry.stale ||
|
|
|
|
(revision !== undefined && (entry.revision === undefined || revision > entry.revision)) ||
|
|
|
|
(timemodified !== undefined && (entry.timemodified === undefined || timemodified > entry.timemodified));
|
2020-10-07 10:53:19 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Check if cannot determine if a file has been updated.
|
|
|
|
*
|
|
|
|
* @param entry Filepool entry.
|
|
|
|
* @return Whether it cannot determine updates.
|
|
|
|
*/
|
2020-10-14 16:38:24 +02:00
|
|
|
protected isFileUpdateUnknown(entry: CoreFilepoolFileEntry): boolean {
|
2020-10-07 10:53:19 +02:00
|
|
|
return !!entry.isexternalfile || (!entry.revision && !entry.timemodified);
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Notify an action performed on a file to a list of components.
|
|
|
|
*
|
|
|
|
* @param siteId The site ID.
|
|
|
|
* @param eventData The file event data.
|
|
|
|
* @param links The links to the components.
|
|
|
|
*/
|
2020-10-21 16:32:27 +02:00
|
|
|
protected notifyFileActionToComponents(
|
|
|
|
siteId: string,
|
|
|
|
eventData: CoreFilepoolFileEventData,
|
|
|
|
links: CoreFilepoolComponentLink[],
|
|
|
|
): void {
|
2020-10-07 10:53:19 +02:00
|
|
|
links.forEach((link) => {
|
|
|
|
const data: CoreFilepoolComponentFileEventData = Object.assign({
|
|
|
|
component: link.component,
|
|
|
|
componentId: link.componentId,
|
|
|
|
}, eventData);
|
|
|
|
|
2020-10-22 12:48:23 +02:00
|
|
|
CoreEvents.trigger(CoreEvents.COMPONENT_FILE_ACTION, data, siteId);
|
2020-10-07 10:53:19 +02:00
|
|
|
});
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Notify a file has been deleted.
|
|
|
|
*
|
|
|
|
* @param siteId The site ID.
|
|
|
|
* @param fileId The file ID.
|
|
|
|
* @param links The links to components.
|
|
|
|
*/
|
|
|
|
protected notifyFileDeleted(siteId: string, fileId: string, links: CoreFilepoolComponentLink[]): void {
|
|
|
|
const data: CoreFilepoolFileEventData = {
|
|
|
|
fileId,
|
|
|
|
action: CoreFilepoolFileActions.DELETED,
|
|
|
|
};
|
|
|
|
|
2020-10-22 12:48:23 +02:00
|
|
|
CoreEvents.trigger(this.getFileEventName(siteId, fileId), data);
|
2020-10-07 10:53:19 +02:00
|
|
|
this.notifyFileActionToComponents(siteId, data, links);
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Notify a file has been downloaded.
|
|
|
|
*
|
|
|
|
* @param siteId The site ID.
|
|
|
|
* @param fileId The file ID.
|
|
|
|
* @param links The links to components.
|
|
|
|
*/
|
|
|
|
protected notifyFileDownloaded(siteId: string, fileId: string, links: CoreFilepoolComponentLink[]): void {
|
|
|
|
const data: CoreFilepoolFileEventData = {
|
|
|
|
fileId,
|
|
|
|
action: CoreFilepoolFileActions.DOWNLOAD,
|
|
|
|
success: true,
|
|
|
|
};
|
|
|
|
|
2020-10-22 12:48:23 +02:00
|
|
|
CoreEvents.trigger(this.getFileEventName(siteId, fileId), data);
|
2020-10-07 10:53:19 +02:00
|
|
|
this.notifyFileActionToComponents(siteId, data, links);
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Notify error occurred while downloading a file.
|
|
|
|
*
|
|
|
|
* @param siteId The site ID.
|
|
|
|
* @param fileId The file ID.
|
|
|
|
* @param links The links to components.
|
|
|
|
*/
|
|
|
|
protected notifyFileDownloadError(siteId: string, fileId: string, links: CoreFilepoolComponentLink[]): void {
|
|
|
|
const data: CoreFilepoolFileEventData = {
|
|
|
|
fileId,
|
|
|
|
action: CoreFilepoolFileActions.DOWNLOAD,
|
|
|
|
success: false,
|
|
|
|
};
|
|
|
|
|
2020-10-22 12:48:23 +02:00
|
|
|
CoreEvents.trigger(this.getFileEventName(siteId, fileId), data);
|
2020-10-07 10:53:19 +02:00
|
|
|
this.notifyFileActionToComponents(siteId, data, links);
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Notify a file starts being downloaded or added to queue.
|
|
|
|
*
|
|
|
|
* @param siteId The site ID.
|
|
|
|
* @param fileId The file ID.
|
|
|
|
* @param links The links to components.
|
|
|
|
*/
|
|
|
|
protected notifyFileDownloading(siteId: string, fileId: string, links: CoreFilepoolComponentLink[]): void {
|
|
|
|
const data: CoreFilepoolFileEventData = {
|
|
|
|
fileId,
|
|
|
|
action: CoreFilepoolFileActions.DOWNLOADING,
|
|
|
|
};
|
|
|
|
|
2020-10-22 12:48:23 +02:00
|
|
|
CoreEvents.trigger(this.getFileEventName(siteId, fileId), data);
|
2020-10-07 10:53:19 +02:00
|
|
|
this.notifyFileActionToComponents(siteId, data, links);
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Notify a file has been outdated.
|
|
|
|
*
|
|
|
|
* @param siteId The site ID.
|
|
|
|
* @param fileId The file ID.
|
|
|
|
* @param links The links to components.
|
|
|
|
*/
|
|
|
|
protected notifyFileOutdated(siteId: string, fileId: string, links: CoreFilepoolComponentLink[]): void {
|
|
|
|
const data: CoreFilepoolFileEventData = {
|
|
|
|
fileId,
|
|
|
|
action: CoreFilepoolFileActions.OUTDATED,
|
|
|
|
};
|
|
|
|
|
2020-10-22 12:48:23 +02:00
|
|
|
CoreEvents.trigger(this.getFileEventName(siteId, fileId), data);
|
2020-10-07 10:53:19 +02:00
|
|
|
this.notifyFileActionToComponents(siteId, data, links);
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Prefetches a list of files.
|
|
|
|
*
|
|
|
|
* @param siteId The site ID.
|
|
|
|
* @param fileList List of files to download.
|
|
|
|
* @param component The component to link the file to.
|
|
|
|
* @param componentId An ID to identify the download.
|
|
|
|
* @param extra Extra data to store for the package.
|
|
|
|
* @param dirPath Name of the directory where to store the files (inside filepool dir). If not defined, store
|
|
|
|
* the files directly inside the filepool folder.
|
|
|
|
* @param onProgress Function to call on progress.
|
|
|
|
* @return Promise resolved when all files are downloaded.
|
|
|
|
*/
|
2020-10-21 16:32:27 +02:00
|
|
|
prefetchPackage(
|
|
|
|
siteId: string,
|
|
|
|
fileList: CoreWSExternalFile[],
|
|
|
|
component: string,
|
|
|
|
componentId?: string | number,
|
|
|
|
extra?: string,
|
|
|
|
dirPath?: string,
|
|
|
|
onProgress?: CoreFilepoolOnProgressCallback,
|
|
|
|
): Promise<void> {
|
2020-10-07 10:53:19 +02:00
|
|
|
return this.downloadOrPrefetchPackage(siteId, fileList, true, component, componentId, extra, dirPath, onProgress);
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Process the queue.
|
|
|
|
*
|
|
|
|
* @description
|
|
|
|
* This loops over itself to keep on processing the queue in the background.
|
|
|
|
* The queue process is site agnostic.
|
|
|
|
*/
|
2020-10-21 16:32:27 +02:00
|
|
|
protected async processQueue(): Promise<void> {
|
|
|
|
try {
|
|
|
|
if (this.queueState !== CoreFilepoolProvider.QUEUE_RUNNING) {
|
|
|
|
// Silently ignore, the queue is on pause.
|
|
|
|
throw CoreFilepoolProvider.ERR_QUEUE_ON_PAUSE;
|
2021-03-02 11:41:04 +01:00
|
|
|
} else if (!CoreFile.isAvailable() || !CoreApp.isOnline()) {
|
2020-10-21 16:32:27 +02:00
|
|
|
throw CoreFilepoolProvider.ERR_FS_OR_NETWORK_UNAVAILABLE;
|
|
|
|
}
|
2020-10-07 10:53:19 +02:00
|
|
|
|
2020-10-21 16:32:27 +02:00
|
|
|
await this.processImportantQueueItem();
|
|
|
|
} catch (error) {
|
2020-10-07 10:53:19 +02:00
|
|
|
// We had an error, in which case we pause the processing.
|
2020-10-08 16:33:10 +02:00
|
|
|
if (error === CoreFilepoolProvider.ERR_FS_OR_NETWORK_UNAVAILABLE) {
|
2020-10-07 10:53:19 +02:00
|
|
|
this.logger.debug('Filesysem or network unavailable, pausing queue processing.');
|
2020-10-08 16:33:10 +02:00
|
|
|
} else if (error === CoreFilepoolProvider.ERR_QUEUE_IS_EMPTY) {
|
2020-10-07 10:53:19 +02:00
|
|
|
this.logger.debug('Queue is empty, pausing queue processing.');
|
|
|
|
}
|
|
|
|
|
2020-10-08 16:33:10 +02:00
|
|
|
this.queueState = CoreFilepoolProvider.QUEUE_PAUSED;
|
2020-10-21 16:32:27 +02:00
|
|
|
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
// All good, we schedule next execution.
|
|
|
|
setTimeout(() => {
|
|
|
|
this.processQueue();
|
|
|
|
}, CoreFilepoolProvider.QUEUE_PROCESS_INTERVAL);
|
2020-10-07 10:53:19 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Process the most important queue item.
|
|
|
|
*
|
|
|
|
* @return Resolved on success. Rejected on failure.
|
|
|
|
*/
|
2020-10-08 16:33:10 +02:00
|
|
|
protected async processImportantQueueItem(): Promise<void> {
|
|
|
|
let items: CoreFilepoolQueueEntry[];
|
2020-12-01 18:37:24 +01:00
|
|
|
const db = await this.appDB;
|
2020-10-07 10:53:19 +02:00
|
|
|
|
|
|
|
try {
|
2020-12-01 18:37:24 +01:00
|
|
|
items = await db.getRecords<CoreFilepoolQueueEntry>(
|
2020-10-28 14:25:18 +01:00
|
|
|
QUEUE_TABLE_NAME,
|
2020-10-21 16:32:27 +02:00
|
|
|
undefined,
|
|
|
|
'priority DESC, added ASC',
|
|
|
|
undefined,
|
|
|
|
0,
|
|
|
|
1,
|
|
|
|
);
|
2020-10-07 10:53:19 +02:00
|
|
|
} catch (err) {
|
2020-10-08 16:33:10 +02:00
|
|
|
throw CoreFilepoolProvider.ERR_QUEUE_IS_EMPTY;
|
2020-10-07 10:53:19 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
const item = items.pop();
|
|
|
|
if (!item) {
|
2020-10-08 16:33:10 +02:00
|
|
|
throw CoreFilepoolProvider.ERR_QUEUE_IS_EMPTY;
|
2020-10-07 10:53:19 +02:00
|
|
|
}
|
|
|
|
// Convert the links to an object.
|
2021-03-02 11:41:04 +01:00
|
|
|
item.linksUnserialized = <CoreFilepoolComponentLink[]> CoreTextUtils.parseJSON(item.links, []);
|
2020-10-07 10:53:19 +02:00
|
|
|
|
|
|
|
return this.processQueueItem(item);
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Process a queue item.
|
|
|
|
*
|
|
|
|
* @param item The object from the queue store.
|
|
|
|
* @return Resolved on success. Rejected on failure.
|
|
|
|
*/
|
2020-10-08 16:33:10 +02:00
|
|
|
protected async processQueueItem(item: CoreFilepoolQueueEntry): Promise<void> {
|
2020-10-07 10:53:19 +02:00
|
|
|
// Cast optional fields to undefined instead of null.
|
|
|
|
const siteId = item.siteId;
|
|
|
|
const fileId = item.fileId;
|
|
|
|
const fileUrl = item.url;
|
|
|
|
const options = {
|
|
|
|
revision: item.revision || undefined,
|
|
|
|
timemodified: item.timemodified || undefined,
|
|
|
|
isexternalfile: item.isexternalfile || undefined,
|
|
|
|
repositorytype: item.repositorytype || undefined,
|
|
|
|
};
|
|
|
|
const filePath = item.path || undefined;
|
2020-10-08 16:33:10 +02:00
|
|
|
const links = item.linksUnserialized || [];
|
2020-10-07 10:53:19 +02:00
|
|
|
|
|
|
|
this.logger.debug('Processing queue item: ' + siteId + ', ' + fileId);
|
|
|
|
|
2020-10-21 16:32:27 +02:00
|
|
|
let entry: CoreFilepoolFileEntry | undefined;
|
2020-10-08 16:33:10 +02:00
|
|
|
|
2020-10-07 10:53:19 +02:00
|
|
|
// Check if the file is already in pool.
|
2020-10-08 16:33:10 +02:00
|
|
|
try {
|
|
|
|
entry = await this.hasFileInPool(siteId, fileId);
|
|
|
|
} catch (error) {
|
2020-10-07 10:53:19 +02:00
|
|
|
// File not in pool.
|
2020-10-08 16:33:10 +02:00
|
|
|
}
|
2020-10-07 10:53:19 +02:00
|
|
|
|
2020-10-08 16:33:10 +02:00
|
|
|
if (entry && !options.isexternalfile && !this.isFileOutdated(entry, options.revision, options.timemodified)) {
|
|
|
|
// We have the file, it is not stale, we can update links and remove from queue.
|
|
|
|
this.logger.debug('Queued file already in store, ignoring...');
|
|
|
|
this.addFileLinks(siteId, fileId, links).catch(() => {
|
|
|
|
// Ignore errors.
|
|
|
|
});
|
|
|
|
this.removeFromQueue(siteId, fileId).catch(() => {
|
|
|
|
// Ignore errors.
|
|
|
|
}).finally(() => {
|
|
|
|
this.treatQueueDeferred(siteId, fileId, true);
|
|
|
|
});
|
2020-10-07 10:53:19 +02:00
|
|
|
|
2020-10-08 16:33:10 +02:00
|
|
|
return;
|
|
|
|
}
|
2020-10-07 10:53:19 +02:00
|
|
|
|
2020-10-08 16:33:10 +02:00
|
|
|
// The file does not exist, or is stale, ... download it.
|
|
|
|
const onProgress = this.getQueueOnProgress(siteId, fileId);
|
2020-10-07 10:53:19 +02:00
|
|
|
|
2020-10-21 16:32:27 +02:00
|
|
|
try {
|
|
|
|
await this.downloadForPoolByUrl(siteId, fileUrl, options, filePath, onProgress, entry);
|
|
|
|
|
2020-10-08 16:33:10 +02:00
|
|
|
// Success, we add links and remove from queue.
|
2021-03-02 11:41:04 +01:00
|
|
|
CoreUtils.ignoreErrors(this.addFileLinks(siteId, fileId, links));
|
2020-10-07 10:53:19 +02:00
|
|
|
|
2020-10-08 16:33:10 +02:00
|
|
|
this.treatQueueDeferred(siteId, fileId, true);
|
|
|
|
this.notifyFileDownloaded(siteId, fileId, links);
|
2020-10-07 10:53:19 +02:00
|
|
|
|
2020-10-08 16:33:10 +02:00
|
|
|
// Wait for the item to be removed from queue before resolving the promise.
|
|
|
|
// If the item could not be removed from queue we still resolve the promise.
|
2021-03-02 11:41:04 +01:00
|
|
|
await CoreUtils.ignoreErrors(this.removeFromQueue(siteId, fileId));
|
2020-10-21 16:32:27 +02:00
|
|
|
} catch (errorObject) {
|
2020-10-08 16:33:10 +02:00
|
|
|
// Whoops, we have an error...
|
|
|
|
let dropFromQueue = false;
|
|
|
|
|
|
|
|
if (errorObject && errorObject.source === fileUrl) {
|
|
|
|
// This is most likely a FileTransfer error.
|
|
|
|
if (errorObject.code === 1) { // FILE_NOT_FOUND_ERR.
|
|
|
|
// The file was not found, most likely a 404, we remove from queue.
|
|
|
|
dropFromQueue = true;
|
|
|
|
} else if (errorObject.code === 2) { // INVALID_URL_ERR.
|
|
|
|
// The URL is invalid, we drop the file from the queue.
|
|
|
|
dropFromQueue = true;
|
|
|
|
} else if (errorObject.code === 3) { // CONNECTION_ERR.
|
|
|
|
// If there was an HTTP status, then let's remove from the queue.
|
|
|
|
dropFromQueue = true;
|
|
|
|
} else if (errorObject.code === 4) { // ABORTED_ERR.
|
|
|
|
// The transfer was aborted, we will keep the file in queue.
|
|
|
|
} else if (errorObject.code === 5) { // NOT_MODIFIED_ERR.
|
|
|
|
// We have the latest version of the file, HTTP 304 status.
|
|
|
|
dropFromQueue = true;
|
2020-10-07 10:53:19 +02:00
|
|
|
} else {
|
2020-10-08 16:33:10 +02:00
|
|
|
// Any error, let's remove the file from the queue to avoi locking down the queue.
|
2020-10-07 10:53:19 +02:00
|
|
|
dropFromQueue = true;
|
|
|
|
}
|
2020-10-08 16:33:10 +02:00
|
|
|
} else {
|
|
|
|
dropFromQueue = true;
|
|
|
|
}
|
2020-10-07 10:53:19 +02:00
|
|
|
|
2020-10-21 16:32:27 +02:00
|
|
|
let errorMessage: string | undefined;
|
2020-10-08 16:33:10 +02:00
|
|
|
// Some Android devices restrict the amount of usable storage using quotas.
|
|
|
|
// If this quota would be exceeded by the download, it throws an exception.
|
|
|
|
// We catch this exception here, and report a meaningful error message to the user.
|
|
|
|
if (errorObject instanceof FileTransferError && errorObject.exception && errorObject.exception.includes('EDQUOT')) {
|
|
|
|
errorMessage = 'core.course.insufficientavailablequota';
|
|
|
|
}
|
2020-10-07 10:53:19 +02:00
|
|
|
|
2020-10-08 16:33:10 +02:00
|
|
|
if (dropFromQueue) {
|
|
|
|
this.logger.debug('Item dropped from queue due to error: ' + fileUrl, errorObject);
|
2020-10-07 10:53:19 +02:00
|
|
|
|
2021-03-02 11:41:04 +01:00
|
|
|
await CoreUtils.ignoreErrors(this.removeFromQueue(siteId, fileId));
|
2020-10-21 16:32:27 +02:00
|
|
|
|
|
|
|
this.treatQueueDeferred(siteId, fileId, false, errorMessage);
|
|
|
|
this.notifyFileDownloadError(siteId, fileId, links);
|
2020-10-08 16:33:10 +02:00
|
|
|
} else {
|
|
|
|
// We considered the file as legit but did not get it, failure.
|
|
|
|
this.treatQueueDeferred(siteId, fileId, false, errorMessage);
|
|
|
|
this.notifyFileDownloadError(siteId, fileId, links);
|
2020-10-07 10:53:19 +02:00
|
|
|
|
2020-10-21 16:32:27 +02:00
|
|
|
throw errorObject;
|
2020-10-08 16:33:10 +02:00
|
|
|
}
|
2020-10-21 16:32:27 +02:00
|
|
|
}
|
2020-10-07 10:53:19 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Remove a file from the queue.
|
|
|
|
*
|
|
|
|
* @param siteId The site ID.
|
|
|
|
* @param fileId The file ID.
|
|
|
|
* @return Resolved on success. Rejected on failure. It is advised to silently ignore failures.
|
|
|
|
*/
|
2020-10-08 16:33:10 +02:00
|
|
|
protected async removeFromQueue(siteId: string, fileId: string): Promise<void> {
|
2020-12-01 18:37:24 +01:00
|
|
|
const db = await this.appDB;
|
2020-10-07 10:53:19 +02:00
|
|
|
|
2020-12-01 18:37:24 +01:00
|
|
|
await db.deleteRecords(QUEUE_TABLE_NAME, { siteId, fileId });
|
2020-10-07 10:53:19 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Remove a file from the pool.
|
|
|
|
*
|
|
|
|
* @param siteId The site ID.
|
|
|
|
* @param fileId The file ID.
|
|
|
|
* @return Resolved on success.
|
|
|
|
*/
|
2020-10-08 16:33:10 +02:00
|
|
|
protected async removeFileById(siteId: string, fileId: string): Promise<void> {
|
2021-03-02 11:41:04 +01:00
|
|
|
const db = await CoreSites.getSiteDb(siteId);
|
2020-10-08 16:33:10 +02:00
|
|
|
// Get the path to the file first since it relies on the file object stored in the pool.
|
|
|
|
// Don't use getFilePath to prevent performing 2 DB requests.
|
|
|
|
let path = this.getFilepoolFolderPath(siteId) + '/' + fileId;
|
2020-10-21 16:32:27 +02:00
|
|
|
let fileUrl: string | undefined;
|
2020-10-07 10:53:19 +02:00
|
|
|
|
2020-10-08 16:33:10 +02:00
|
|
|
try {
|
|
|
|
const entry = await this.hasFileInPool(siteId, fileId);
|
2020-10-07 10:53:19 +02:00
|
|
|
|
2020-10-21 16:32:27 +02:00
|
|
|
fileUrl = entry.url;
|
2020-10-08 16:33:10 +02:00
|
|
|
if (entry.extension) {
|
|
|
|
path += '.' + entry.extension;
|
|
|
|
}
|
|
|
|
} catch (error) {
|
|
|
|
// If file not found, use the path without extension.
|
|
|
|
}
|
2020-10-07 10:53:19 +02:00
|
|
|
|
2020-10-08 16:33:10 +02:00
|
|
|
const conditions = {
|
|
|
|
fileId,
|
|
|
|
};
|
2020-10-07 10:53:19 +02:00
|
|
|
|
2020-10-08 16:33:10 +02:00
|
|
|
// Get links to components to notify them after remove.
|
|
|
|
const links = await this.getFileLinks(siteId, fileId);
|
2020-10-21 16:32:27 +02:00
|
|
|
const promises: Promise<unknown>[] = [];
|
2020-10-07 10:53:19 +02:00
|
|
|
|
2020-10-08 16:33:10 +02:00
|
|
|
// Remove entry from filepool store.
|
2020-10-28 14:25:18 +01:00
|
|
|
promises.push(db.deleteRecords(FILES_TABLE_NAME, conditions));
|
2020-10-07 10:53:19 +02:00
|
|
|
|
2020-10-08 16:33:10 +02:00
|
|
|
// Remove links.
|
2020-10-28 14:25:18 +01:00
|
|
|
promises.push(db.deleteRecords(LINKS_TABLE_NAME, conditions));
|
2020-10-08 16:33:10 +02:00
|
|
|
|
|
|
|
// Remove the file.
|
2021-03-02 11:41:04 +01:00
|
|
|
if (CoreFile.isAvailable()) {
|
|
|
|
promises.push(CoreFile.removeFile(path).catch((error) => {
|
2020-10-08 16:33:10 +02:00
|
|
|
if (error && error.code == 1) {
|
|
|
|
// Not found, ignore error since maybe it was deleted already.
|
|
|
|
} else {
|
2020-10-21 16:32:27 +02:00
|
|
|
throw error;
|
2020-10-08 16:33:10 +02:00
|
|
|
}
|
|
|
|
}));
|
|
|
|
}
|
|
|
|
|
|
|
|
await Promise.all(promises);
|
|
|
|
|
|
|
|
this.notifyFileDeleted(siteId, fileId, links);
|
|
|
|
|
2020-10-21 16:32:27 +02:00
|
|
|
if (fileUrl) {
|
2021-03-02 11:41:04 +01:00
|
|
|
await CoreUtils.ignoreErrors(CorePluginFileDelegate.fileDeleted(fileUrl, path, siteId));
|
2020-10-08 16:33:10 +02:00
|
|
|
}
|
2020-10-07 10:53:19 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Delete all the matching files from a component.
|
|
|
|
*
|
|
|
|
* @param siteId The site ID.
|
|
|
|
* @param component The component to link the file to.
|
|
|
|
* @param componentId An ID to use in conjunction with the component.
|
|
|
|
* @return Resolved on success.
|
|
|
|
*/
|
2020-10-08 16:33:10 +02:00
|
|
|
async removeFilesByComponent(siteId: string, component: string, componentId?: string | number): Promise<void> {
|
2021-03-02 11:41:04 +01:00
|
|
|
const db = await CoreSites.getSiteDb(siteId);
|
2020-10-08 16:33:10 +02:00
|
|
|
const items = await this.getComponentFiles(db, component, componentId);
|
|
|
|
|
|
|
|
await Promise.all(items.map((item) => this.removeFileById(siteId, item.fileId)));
|
2020-10-07 10:53:19 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Remove a file from the pool.
|
|
|
|
*
|
|
|
|
* @param siteId The site ID.
|
|
|
|
* @param fileUrl The file URL.
|
|
|
|
* @return Resolved on success, rejected on failure.
|
|
|
|
*/
|
2020-10-08 16:33:10 +02:00
|
|
|
async removeFileByUrl(siteId: string, fileUrl: string): Promise<void> {
|
|
|
|
const file = await this.fixPluginfileURL(siteId, fileUrl);
|
|
|
|
const fileId = this.getFileIdByUrl(file.fileurl);
|
2020-10-07 10:53:19 +02:00
|
|
|
|
2020-10-08 16:33:10 +02:00
|
|
|
await this.removeFileById(siteId, fileId);
|
2020-10-07 10:53:19 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Removes the revision number from a file URL.
|
|
|
|
*
|
|
|
|
* @param url URL to remove the revision number.
|
|
|
|
* @return URL without revision number.
|
|
|
|
* @description
|
|
|
|
* The revision is used to know if a file has changed. We remove it from the URL to prevent storing a file per revision.
|
|
|
|
*/
|
|
|
|
protected removeRevisionFromUrl(url: string): string {
|
|
|
|
const args = this.getPluginFileArgs(url);
|
|
|
|
if (!args) {
|
|
|
|
// Not a pluginfile, no revision will be found.
|
|
|
|
return url;
|
|
|
|
}
|
|
|
|
|
2021-03-02 11:41:04 +01:00
|
|
|
return CorePluginFileDelegate.removeRevisionFromUrl(url, args);
|
2020-10-07 10:53:19 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Change the package status, setting it to the previous status.
|
|
|
|
*
|
|
|
|
* @param siteId Site ID.
|
|
|
|
* @param component Package's component.
|
|
|
|
* @param componentId An ID to use in conjunction with the component.
|
|
|
|
* @return Promise resolved when the status is changed. Resolve param: new status.
|
|
|
|
*/
|
2020-10-08 16:33:10 +02:00
|
|
|
async setPackagePreviousStatus(siteId: string, component: string, componentId?: string | number): Promise<string> {
|
2020-10-07 10:53:19 +02:00
|
|
|
componentId = this.fixComponentId(componentId);
|
|
|
|
this.logger.debug(`Set previous status for package ${component} ${componentId}`);
|
|
|
|
|
2021-03-02 11:41:04 +01:00
|
|
|
const site = await CoreSites.getSite(siteId);
|
2020-10-08 16:33:10 +02:00
|
|
|
const packageId = this.getPackageId(component, componentId);
|
2020-10-07 10:53:19 +02:00
|
|
|
|
2020-10-08 16:33:10 +02:00
|
|
|
// Get current stored data, we'll only update 'status' and 'updated' fields.
|
2020-10-28 14:25:18 +01:00
|
|
|
const entry = <CoreFilepoolPackageEntry> site.getDb().getRecord(PACKAGES_TABLE_NAME, { id: packageId });
|
2020-10-08 16:33:10 +02:00
|
|
|
const newData: CoreFilepoolPackageEntry = {};
|
|
|
|
if (entry.status == CoreConstants.DOWNLOADING) {
|
|
|
|
// Going back from downloading to previous status, restore previous download time.
|
|
|
|
newData.downloadTime = entry.previousDownloadTime;
|
|
|
|
}
|
|
|
|
newData.status = entry.previous || CoreConstants.NOT_DOWNLOADED;
|
|
|
|
newData.updated = Date.now();
|
|
|
|
this.logger.debug(`Set previous status '${entry.status}' for package ${component} ${componentId}`);
|
2020-10-07 10:53:19 +02:00
|
|
|
|
2020-10-28 14:25:18 +01:00
|
|
|
await site.getDb().updateRecords(PACKAGES_TABLE_NAME, newData, { id: packageId });
|
2020-10-08 16:33:10 +02:00
|
|
|
// Success updating, trigger event.
|
2020-10-21 16:32:27 +02:00
|
|
|
this.triggerPackageStatusChanged(site.id!, newData.status, component, componentId);
|
2020-10-07 10:53:19 +02:00
|
|
|
|
2020-10-08 16:33:10 +02:00
|
|
|
return newData.status;
|
2020-10-07 10:53:19 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Check if a file should be downloaded based on its size.
|
|
|
|
*
|
|
|
|
* @param size File size.
|
|
|
|
* @return Whether file should be downloaded.
|
|
|
|
*/
|
|
|
|
shouldDownload(size: number): boolean {
|
2020-10-08 16:33:10 +02:00
|
|
|
return size <= CoreFilepoolProvider.DOWNLOAD_THRESHOLD ||
|
2021-03-02 11:41:04 +01:00
|
|
|
(CoreApp.isWifi() && size <= CoreFilepoolProvider.WIFI_DOWNLOAD_THRESHOLD);
|
2020-10-07 10:53:19 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Convenience function to check if a file should be downloaded before opening it.
|
|
|
|
*
|
|
|
|
* @param url File online URL.
|
|
|
|
* @param size File size.
|
|
|
|
* @return Promise resolved if should download before open, rejected otherwise.
|
2021-01-20 15:33:41 +01:00
|
|
|
* @ddeprecated since 3.9.5. Please use shouldDownloadFileBeforeOpen instead.
|
|
|
|
*/
|
|
|
|
async shouldDownloadBeforeOpen(url: string, size: number): Promise<void> {
|
|
|
|
if (size >= 0 && size <= CoreFilepoolProvider.DOWNLOAD_THRESHOLD) {
|
|
|
|
// The file is small, download it.
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
2021-03-02 11:41:04 +01:00
|
|
|
const mimetype = await CoreUtils.getMimeTypeFromUrl(url);
|
2021-01-20 15:33:41 +01:00
|
|
|
// If the file is streaming (audio or video) we reject.
|
|
|
|
if (mimetype.indexOf('video') != -1 || mimetype.indexOf('audio') != -1) {
|
|
|
|
throw new CoreError('File is audio or video.');
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Convenience function to check if a file should be downloaded before opening it.
|
|
|
|
*
|
|
|
|
* @param url File online URL.
|
|
|
|
* @param size File size.
|
|
|
|
* @return Promise resolved with boolean: whether file should be downloaded before opening it.
|
2020-10-07 10:53:19 +02:00
|
|
|
* @description
|
|
|
|
* Convenience function to check if a file should be downloaded before opening it.
|
|
|
|
*
|
|
|
|
* The default behaviour in the app is to download first and then open the local file in the following cases:
|
|
|
|
* - The file is small (less than DOWNLOAD_THRESHOLD).
|
|
|
|
* - The file cannot be streamed.
|
|
|
|
* If the file is big and can be streamed, the promise returned by this function will be rejected.
|
|
|
|
*/
|
2021-01-20 15:33:41 +01:00
|
|
|
async shouldDownloadFileBeforeOpen(url: string, size: number): Promise<boolean> {
|
2020-10-08 16:33:10 +02:00
|
|
|
if (size >= 0 && size <= CoreFilepoolProvider.DOWNLOAD_THRESHOLD) {
|
2020-10-07 10:53:19 +02:00
|
|
|
// The file is small, download it.
|
2021-01-20 15:33:41 +01:00
|
|
|
return true;
|
2020-10-07 10:53:19 +02:00
|
|
|
}
|
|
|
|
|
2021-03-02 11:41:04 +01:00
|
|
|
const mimetype = await CoreUtils.getMimeTypeFromUrl(url);
|
2021-01-20 15:33:41 +01:00
|
|
|
|
|
|
|
// If the file is streaming (audio or video), return false.
|
|
|
|
return mimetype.indexOf('video') == -1 && mimetype.indexOf('audio') == -1;
|
2020-10-07 10:53:19 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Store package status.
|
|
|
|
*
|
|
|
|
* @param siteId Site ID.
|
|
|
|
* @param status New package status.
|
|
|
|
* @param component Package's component.
|
|
|
|
* @param componentId An ID to use in conjunction with the component.
|
|
|
|
* @param extra Extra data to store for the package. If you want to store more than 1 value, use JSON.stringify.
|
|
|
|
* @return Promise resolved when status is stored.
|
|
|
|
*/
|
2020-10-21 16:32:27 +02:00
|
|
|
async storePackageStatus(
|
|
|
|
siteId: string,
|
|
|
|
status: string,
|
|
|
|
component: string,
|
|
|
|
componentId?: string | number,
|
|
|
|
extra?: string,
|
|
|
|
): Promise<void> {
|
2020-10-07 10:53:19 +02:00
|
|
|
this.logger.debug(`Set status '${status}' for package ${component} ${componentId}`);
|
|
|
|
componentId = this.fixComponentId(componentId);
|
|
|
|
|
2021-03-02 11:41:04 +01:00
|
|
|
const site = await CoreSites.getSite(siteId);
|
2020-10-08 16:33:10 +02:00
|
|
|
const packageId = this.getPackageId(component, componentId);
|
2020-10-21 16:32:27 +02:00
|
|
|
let downloadTime: number | undefined;
|
|
|
|
let previousDownloadTime: number | undefined;
|
2020-10-07 10:53:19 +02:00
|
|
|
|
2020-10-08 16:33:10 +02:00
|
|
|
if (status == CoreConstants.DOWNLOADING) {
|
|
|
|
// Set download time if package is now downloading.
|
2021-03-02 11:41:04 +01:00
|
|
|
downloadTime = CoreTimeUtils.timestamp();
|
2020-10-08 16:33:10 +02:00
|
|
|
}
|
|
|
|
|
2020-10-21 16:32:27 +02:00
|
|
|
let previousStatus: string | undefined;
|
2020-10-08 16:33:10 +02:00
|
|
|
// Search current status to set it as previous status.
|
|
|
|
try {
|
2021-02-18 09:19:38 +01:00
|
|
|
const entry = await site.getDb().getRecord<CoreFilepoolPackageEntry>(PACKAGES_TABLE_NAME, { id: packageId });
|
|
|
|
|
|
|
|
extra = extra ?? entry.extra;
|
2020-10-08 16:33:10 +02:00
|
|
|
if (typeof downloadTime == 'undefined') {
|
2021-02-18 09:19:38 +01:00
|
|
|
// Keep previous download time.
|
2020-10-08 16:33:10 +02:00
|
|
|
downloadTime = entry.downloadTime;
|
|
|
|
previousDownloadTime = entry.previousDownloadTime;
|
|
|
|
} else {
|
2021-02-18 09:19:38 +01:00
|
|
|
// The downloadTime will be updated, store current time as previous.
|
2020-10-08 16:33:10 +02:00
|
|
|
previousDownloadTime = entry.downloadTime;
|
2020-10-07 10:53:19 +02:00
|
|
|
}
|
|
|
|
|
2020-10-08 16:33:10 +02:00
|
|
|
previousStatus = entry.status;
|
|
|
|
} catch (error) {
|
|
|
|
// No previous status.
|
|
|
|
}
|
2020-10-07 10:53:19 +02:00
|
|
|
|
2020-10-08 16:33:10 +02:00
|
|
|
const packageEntry: CoreFilepoolPackageEntry = {
|
|
|
|
id: packageId,
|
|
|
|
component,
|
|
|
|
componentId,
|
|
|
|
status,
|
|
|
|
previous: previousStatus,
|
|
|
|
updated: Date.now(),
|
|
|
|
downloadTime,
|
|
|
|
previousDownloadTime,
|
|
|
|
extra,
|
|
|
|
};
|
2020-10-07 10:53:19 +02:00
|
|
|
|
2020-10-08 16:33:10 +02:00
|
|
|
if (previousStatus === status) {
|
|
|
|
// The package already has this status, no need to change it.
|
|
|
|
return;
|
|
|
|
}
|
2020-10-07 10:53:19 +02:00
|
|
|
|
2020-10-28 14:25:18 +01:00
|
|
|
await site.getDb().insertRecord(PACKAGES_TABLE_NAME, packageEntry);
|
2020-10-08 16:33:10 +02:00
|
|
|
|
|
|
|
// Success inserting, trigger event.
|
|
|
|
this.triggerPackageStatusChanged(siteId, status, component, componentId);
|
2020-10-07 10:53:19 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Search for files in a CSS code and try to download them. Once downloaded, replace their URLs
|
|
|
|
* and store the result in the CSS file.
|
|
|
|
*
|
|
|
|
* @param siteId Site ID.
|
|
|
|
* @param fileUrl CSS file URL.
|
|
|
|
* @param cssCode CSS code.
|
|
|
|
* @param component The component to link the file to.
|
|
|
|
* @param componentId An ID to use in conjunction with the component.
|
|
|
|
* @param revision Revision to use in all files. If not defined, it will be calculated using the URL of each file.
|
|
|
|
* @return Promise resolved with the CSS code.
|
|
|
|
*/
|
2020-10-21 16:32:27 +02:00
|
|
|
async treatCSSCode(
|
|
|
|
siteId: string,
|
|
|
|
fileUrl: string,
|
|
|
|
cssCode: string,
|
|
|
|
component?: string,
|
|
|
|
componentId?: string | number,
|
|
|
|
revision?: number,
|
|
|
|
): Promise<string> {
|
2021-03-02 11:41:04 +01:00
|
|
|
const urls = CoreDomUtils.extractUrlsFromCSS(cssCode);
|
2020-10-07 10:53:19 +02:00
|
|
|
let updated = false;
|
|
|
|
|
|
|
|
// Get the path of the CSS file.
|
2020-10-21 16:32:27 +02:00
|
|
|
const filePath = await this.getFilePathByUrl(siteId, fileUrl);
|
2020-10-07 10:53:19 +02:00
|
|
|
|
2020-10-21 16:32:27 +02:00
|
|
|
// Download all files in the CSS.
|
|
|
|
await Promise.all(urls.map(async (url) => {
|
2020-10-07 10:53:19 +02:00
|
|
|
// Download the file only if it's an online URL.
|
2021-03-02 11:41:04 +01:00
|
|
|
if (CoreUrlUtils.isLocalFileUrl(url)) {
|
2020-10-21 16:32:27 +02:00
|
|
|
return;
|
2020-10-07 10:53:19 +02:00
|
|
|
}
|
|
|
|
|
2020-10-21 16:32:27 +02:00
|
|
|
try {
|
|
|
|
const fileUrl = await this.downloadUrl(
|
|
|
|
siteId,
|
|
|
|
url,
|
|
|
|
false,
|
|
|
|
component,
|
|
|
|
componentId,
|
|
|
|
0,
|
|
|
|
undefined,
|
|
|
|
undefined,
|
|
|
|
undefined,
|
|
|
|
revision,
|
|
|
|
);
|
|
|
|
|
|
|
|
if (fileUrl != url) {
|
2021-03-02 11:41:04 +01:00
|
|
|
cssCode = cssCode.replace(new RegExp(CoreTextUtils.escapeForRegex(url), 'g'), fileUrl);
|
2020-10-21 16:32:27 +02:00
|
|
|
updated = true;
|
|
|
|
}
|
|
|
|
} catch (error) {
|
|
|
|
this.logger.warn('Error treating file ', url, error);
|
2020-10-07 10:53:19 +02:00
|
|
|
}
|
2020-10-21 16:32:27 +02:00
|
|
|
}));
|
|
|
|
|
|
|
|
// All files downloaded. Store the result if it has changed.
|
|
|
|
if (updated) {
|
2021-03-02 11:41:04 +01:00
|
|
|
await CoreFile.writeFile(filePath, cssCode);
|
2020-10-21 16:32:27 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
return cssCode;
|
2020-10-07 10:53:19 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Resolves or rejects a queue deferred and removes it from the list.
|
|
|
|
*
|
|
|
|
* @param siteId The site ID.
|
|
|
|
* @param fileId The file ID.
|
|
|
|
* @param resolve True if promise should be resolved, false if it should be rejected.
|
|
|
|
* @param error String identifier for error message, if rejected.
|
|
|
|
*/
|
|
|
|
protected treatQueueDeferred(siteId: string, fileId: string, resolve: boolean, error?: string): void {
|
|
|
|
if (this.queueDeferreds[siteId] && this.queueDeferreds[siteId][fileId]) {
|
|
|
|
if (resolve) {
|
|
|
|
this.queueDeferreds[siteId][fileId].resolve();
|
|
|
|
} else {
|
|
|
|
this.queueDeferreds[siteId][fileId].reject(error);
|
|
|
|
}
|
|
|
|
delete this.queueDeferreds[siteId][fileId];
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Trigger mmCoreEventPackageStatusChanged with the right data.
|
|
|
|
*
|
|
|
|
* @param siteId Site ID.
|
|
|
|
* @param status New package status.
|
|
|
|
* @param component Package's component.
|
|
|
|
* @param componentId An ID to use in conjunction with the component.
|
|
|
|
*/
|
2020-12-11 15:40:34 +01:00
|
|
|
protected triggerPackageStatusChanged(siteId: string, status: string, component: string, componentId?: string | number): void {
|
|
|
|
const data: CoreEventPackageStatusChanged = {
|
2020-10-07 10:53:19 +02:00
|
|
|
component,
|
|
|
|
componentId: this.fixComponentId(componentId),
|
|
|
|
status,
|
|
|
|
};
|
2020-10-08 16:33:10 +02:00
|
|
|
|
2020-10-22 12:48:23 +02:00
|
|
|
CoreEvents.trigger(CoreEvents.PACKAGE_STATUS_CHANGED, data, siteId);
|
2020-10-07 10:53:19 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Update the download time of a package. This doesn't modify the previous download time.
|
|
|
|
* This function should be used if a package generates some new data during a download. Calling this function
|
|
|
|
* right after generating the data in the download will prevent detecting this data as an update.
|
|
|
|
*
|
|
|
|
* @param siteId Site ID.
|
|
|
|
* @param component Package's component.
|
|
|
|
* @param componentId An ID to use in conjunction with the component.
|
|
|
|
* @return Promise resolved when status is stored.
|
|
|
|
*/
|
2020-10-08 16:33:10 +02:00
|
|
|
async updatePackageDownloadTime(siteId: string, component: string, componentId?: string | number): Promise<void> {
|
2020-10-07 10:53:19 +02:00
|
|
|
componentId = this.fixComponentId(componentId);
|
|
|
|
|
2021-03-02 11:41:04 +01:00
|
|
|
const site = await CoreSites.getSite(siteId);
|
2020-10-08 16:33:10 +02:00
|
|
|
const packageId = this.getPackageId(component, componentId);
|
2020-10-07 10:53:19 +02:00
|
|
|
|
2020-10-21 16:32:27 +02:00
|
|
|
await site.getDb().updateRecords(
|
2020-10-28 14:25:18 +01:00
|
|
|
PACKAGES_TABLE_NAME,
|
2021-03-02 11:41:04 +01:00
|
|
|
{ downloadTime: CoreTimeUtils.timestamp() },
|
2020-10-21 16:32:27 +02:00
|
|
|
{ id: packageId },
|
|
|
|
);
|
2020-10-07 10:53:19 +02:00
|
|
|
}
|
2020-10-08 16:33:10 +02:00
|
|
|
|
2020-10-07 10:53:19 +02:00
|
|
|
}
|
|
|
|
|
2021-03-02 11:41:04 +01:00
|
|
|
export const CoreFilepool = makeSingleton(CoreFilepoolProvider);
|
2020-10-07 10:53:19 +02:00
|
|
|
|
|
|
|
/**
|
|
|
|
* File actions.
|
|
|
|
*/
|
|
|
|
export const enum CoreFilepoolFileActions {
|
|
|
|
DOWNLOAD = 'download',
|
|
|
|
DOWNLOADING = 'downloading',
|
|
|
|
DELETED = 'deleted',
|
|
|
|
OUTDATED = 'outdated',
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Data sent to file events.
|
|
|
|
*/
|
|
|
|
export type CoreFilepoolFileEventData = {
|
|
|
|
/**
|
|
|
|
* The file ID.
|
|
|
|
*/
|
|
|
|
fileId: string;
|
|
|
|
|
|
|
|
/**
|
|
|
|
* The file ID.
|
|
|
|
*/
|
|
|
|
action: CoreFilepoolFileActions;
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Whether the action was a success. Only for DOWNLOAD action.
|
|
|
|
*/
|
|
|
|
success?: boolean;
|
|
|
|
};
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Data sent to component file events.
|
|
|
|
*/
|
|
|
|
export type CoreFilepoolComponentFileEventData = CoreFilepoolFileEventData & {
|
|
|
|
/**
|
|
|
|
* The component.
|
|
|
|
*/
|
|
|
|
component: string;
|
|
|
|
|
|
|
|
/**
|
|
|
|
* The component ID.
|
|
|
|
*/
|
2020-10-21 16:32:27 +02:00
|
|
|
componentId?: string | number;
|
2020-10-07 10:53:19 +02:00
|
|
|
};
|
2020-10-08 16:33:10 +02:00
|
|
|
|
|
|
|
/**
|
|
|
|
* Function called when file download progress ocurred.
|
|
|
|
*/
|
|
|
|
export type CoreFilepoolOnProgressCallback<T = unknown> = (event: T) => void;
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Deferred promise for file pool. It's similar to the result of $q.defer() in AngularJS.
|
|
|
|
*/
|
|
|
|
type CoreFilepoolPromiseDefer = PromiseDefer<void> & {
|
|
|
|
onProgress?: CoreFilepoolOnProgressCallback; // On Progress function.
|
|
|
|
};
|
|
|
|
|
2020-10-21 16:32:27 +02:00
|
|
|
type AnchorOrMediaElement =
|
|
|
|
HTMLAnchorElement | HTMLImageElement | HTMLAudioElement | HTMLVideoElement | HTMLSourceElement | HTMLTrackElement;
|