MOBILE-3977 filepool: Optimize files table

main
Noel De Martin 2022-02-03 13:23:08 +01:00
parent 808a242cbc
commit 7c834281ce
5 changed files with 337 additions and 161 deletions

View File

@ -271,6 +271,7 @@ testsConfig['rules']['padded-blocks'] = [
switches: 'never', switches: 'never',
}, },
]; ];
testsConfig['rules']['jest/expect-expect'] = 'off';
testsConfig['plugins'].push('jest'); testsConfig['plugins'].push('jest');
testsConfig['extends'].push('plugin:jest/recommended'); testsConfig['extends'].push('plugin:jest/recommended');

View File

@ -16,6 +16,7 @@ import { CorePromisedValue } from '@classes/promised-value';
import { SQLiteDB, SQLiteDBRecordValues } from '@classes/sqlitedb'; import { SQLiteDB, SQLiteDBRecordValues } from '@classes/sqlitedb';
import { CoreDatabaseReducer, CoreDatabaseTable, CoreDatabaseConditions, GetDBRecordPrimaryKey } from './database-table'; import { CoreDatabaseReducer, CoreDatabaseTable, CoreDatabaseConditions, GetDBRecordPrimaryKey } from './database-table';
import { CoreEagerDatabaseTable } from './eager-database-table'; import { CoreEagerDatabaseTable } from './eager-database-table';
import { CoreLazyDatabaseTable } from './lazy-database-table';
/** /**
* Database table proxy used to route database interactions through different implementations. * Database table proxy used to route database interactions through different implementations.
@ -164,6 +165,8 @@ export class CoreDatabaseTableProxy<
switch (cachingStrategy) { switch (cachingStrategy) {
case CoreDatabaseCachingStrategy.Eager: case CoreDatabaseCachingStrategy.Eager:
return new CoreEagerDatabaseTable(this.database, this.tableName, this.primaryKeyColumns); return new CoreEagerDatabaseTable(this.database, this.tableName, this.primaryKeyColumns);
case CoreDatabaseCachingStrategy.Lazy:
return new CoreLazyDatabaseTable(this.database, this.tableName, this.primaryKeyColumns);
case CoreDatabaseCachingStrategy.None: case CoreDatabaseCachingStrategy.None:
return new CoreDatabaseTable(this.database, this.tableName, this.primaryKeyColumns); return new CoreDatabaseTable(this.database, this.tableName, this.primaryKeyColumns);
} }
@ -183,5 +186,6 @@ export interface CoreDatabaseConfiguration {
*/ */
export enum CoreDatabaseCachingStrategy { export enum CoreDatabaseCachingStrategy {
Eager = 'eager', Eager = 'eager',
Lazy = 'lazy',
None = 'none', None = 'none',
} }

View File

@ -0,0 +1,141 @@
// (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 { CoreError } from '@classes/errors/error';
import { SQLiteDBRecordValues } from '@classes/sqlitedb';
import { CoreDatabaseTable, CoreDatabaseConditions, GetDBRecordPrimaryKey } from './database-table';
/**
* Wrapper used to improve performance by caching records that are used often for faster read operations.
*
* This implementation works best for tables that have a lot of records and are read often; for tables with a few records use
* CoreEagerDatabaseTable instead.
*/
export class CoreLazyDatabaseTable<
DBRecord extends SQLiteDBRecordValues = SQLiteDBRecordValues,
PrimaryKeyColumn extends keyof DBRecord = 'id',
PrimaryKey extends GetDBRecordPrimaryKey<DBRecord, PrimaryKeyColumn> = GetDBRecordPrimaryKey<DBRecord, PrimaryKeyColumn>
> extends CoreDatabaseTable<DBRecord, PrimaryKeyColumn, PrimaryKey> {
protected records: Record<string, DBRecord | null> = {};
/**
* @inheritdoc
*/
async find(conditions: Partial<DBRecord>): Promise<DBRecord> {
let record: DBRecord | null =
Object.values(this.records).find(record => record && this.recordMatches(record, conditions)) ?? null;
if (!record) {
record = await super.find(conditions);
this.records[this.serializePrimaryKey(this.getPrimaryKeyFromRecord(record))] = record;
}
return record;
}
/**
* @inheritdoc
*/
async findByPrimaryKey(primaryKey: PrimaryKey): Promise<DBRecord> {
const serializePrimaryKey = this.serializePrimaryKey(primaryKey);
if (!(serializePrimaryKey in this.records)) {
try {
const record = await super.findByPrimaryKey(primaryKey);
this.records[serializePrimaryKey] = record;
return record;
} catch (error) {
this.records[serializePrimaryKey] = null;
throw error;
}
}
const record = this.records[serializePrimaryKey];
if (!record) {
throw new CoreError('No records found.');
}
return record;
}
/**
* @inheritdoc
*/
async insert(record: DBRecord): Promise<void> {
await super.insert(record);
this.records[this.serializePrimaryKey(this.getPrimaryKeyFromRecord(record))] = record;
}
/**
* @inheritdoc
*/
async update(updates: Partial<DBRecord>, conditions?: Partial<DBRecord>): Promise<void> {
await super.update(updates, conditions);
for (const record of Object.values(this.records)) {
if (!record || (conditions && !this.recordMatches(record, conditions))) {
continue;
}
Object.assign(record, updates);
}
}
/**
* @inheritdoc
*/
async updateWhere(updates: Partial<DBRecord>, conditions: CoreDatabaseConditions<DBRecord>): Promise<void> {
await super.updateWhere(updates, conditions);
for (const record of Object.values(this.records)) {
if (!record || !conditions.js(record)) {
continue;
}
Object.assign(record, updates);
}
}
/**
* @inheritdoc
*/
async delete(conditions?: Partial<DBRecord>): Promise<void> {
await super.delete(conditions);
for (const [primaryKey, record] of Object.entries(this.records)) {
if (!record || (conditions && !this.recordMatches(record, conditions))) {
continue;
}
this.records[primaryKey] = null;
}
}
/**
* @inheritdoc
*/
async deleteByPrimaryKey(primaryKey: PrimaryKey): Promise<void> {
await super.deleteByPrimaryKey(primaryKey);
this.records[this.serializePrimaryKey(primaryKey)] = null;
}
}

View File

@ -13,6 +13,7 @@
// limitations under the License. // limitations under the License.
import { mock } from '@/testing/utils'; import { mock } from '@/testing/utils';
import { CoreDatabaseTable } from '@classes/database/database-table';
import { import {
CoreDatabaseCachingStrategy, CoreDatabaseCachingStrategy,
CoreDatabaseConfiguration, CoreDatabaseConfiguration,
@ -30,7 +31,7 @@ function userMatches(user: User, conditions: Partial<User>) {
return !Object.entries(conditions).some(([column, value]) => user[column] !== value); return !Object.entries(conditions).some(([column, value]) => user[column] !== value);
} }
function prepareStubs(config: Partial<CoreDatabaseConfiguration> = {}): [User[], SQLiteDB, CoreDatabaseTableProxy<User>] { function prepareStubs(config: Partial<CoreDatabaseConfiguration> = {}): [User[], SQLiteDB, CoreDatabaseTable<User>] {
const records: User[] = []; const records: User[] = [];
const database = mock<SQLiteDB>({ const database = mock<SQLiteDB>({
getRecord: async <T>(_, conditions) => { getRecord: async <T>(_, conditions) => {
@ -68,11 +69,84 @@ function prepareStubs(config: Partial<CoreDatabaseConfiguration> = {}): [User[],
return [records, database, table]; return [records, database, table];
} }
async function testFindItems(records: User[], table: CoreDatabaseTable<User>) {
const john = { id: 1, name: 'John', surname: 'Doe' };
const amy = { id: 2, name: 'Amy', surname: 'Doe' };
records.push(john);
records.push(amy);
await table.initialize();
await expect(table.findByPrimaryKey({ id: 1 })).resolves.toEqual(john);
await expect(table.findByPrimaryKey({ id: 2 })).resolves.toEqual(amy);
await expect(table.find({ surname: 'Doe', name: 'John' })).resolves.toEqual(john);
await expect(table.find({ surname: 'Doe', name: 'Amy' })).resolves.toEqual(amy);
}
async function testInsertItems(records: User[], database: SQLiteDB, table: CoreDatabaseTable<User>) {
// Arrange.
const john = { id: 1, name: 'John', surname: 'Doe' };
await table.initialize();
// Act.
await table.insert(john);
// Assert.
expect(database.insertRecord).toHaveBeenCalledWith('users', john);
await expect(table.findByPrimaryKey({ id: 1 })).resolves.toEqual(john);
}
async function testDeleteItems(records: User[], database: SQLiteDB, table: CoreDatabaseTable<User>) {
// Arrange.
const john = { id: 1, name: 'John', surname: 'Doe' };
const amy = { id: 2, name: 'Amy', surname: 'Doe' };
const jane = { id: 3, name: 'Jane', surname: 'Smith' };
records.push(john);
records.push(amy);
records.push(jane);
await table.initialize();
// Act.
await table.delete({ surname: 'Doe' });
// Assert.
expect(database.deleteRecords).toHaveBeenCalledWith('users', { surname: 'Doe' });
await expect(table.findByPrimaryKey({ id: 1 })).rejects.toThrow();
await expect(table.findByPrimaryKey({ id: 2 })).rejects.toThrow();
await expect(table.findByPrimaryKey({ id: 3 })).resolves.toEqual(jane);
}
async function testDeleteItemsByPrimaryKey(records: User[], database: SQLiteDB, table: CoreDatabaseTable<User>) {
// Arrange.
const john = { id: 1, name: 'John', surname: 'Doe' };
const amy = { id: 2, name: 'Amy', surname: 'Doe' };
records.push(john);
records.push(amy);
await table.initialize();
// Act.
await table.deleteByPrimaryKey({ id: 1 });
// Assert.
expect(database.deleteRecords).toHaveBeenCalledWith('users', { id: 1 });
await expect(table.findByPrimaryKey({ id: 1 })).rejects.toThrow();
await expect(table.findByPrimaryKey({ id: 2 })).resolves.toEqual(amy);
}
describe('CoreDatabaseTable with eager caching', () => { describe('CoreDatabaseTable with eager caching', () => {
let records: User[]; let records: User[];
let database: SQLiteDB; let database: SQLiteDB;
let table: CoreDatabaseTableProxy<User>; let table: CoreDatabaseTable<User>;
beforeEach(() => [records, database, table] = prepareStubs({ cachingStrategy: CoreDatabaseCachingStrategy.Eager })); beforeEach(() => [records, database, table] = prepareStubs({ cachingStrategy: CoreDatabaseCachingStrategy.Eager }));
@ -83,79 +157,41 @@ describe('CoreDatabaseTable with eager caching', () => {
}); });
it('finds items', async () => { it('finds items', async () => {
const john = { id: 1, name: 'John', surname: 'Doe' }; await testFindItems(records, table);
const amy = { id: 2, name: 'Amy', surname: 'Doe' };
records.push(john);
records.push(amy);
await table.initialize();
await expect(table.findByPrimaryKey({ id: 1 })).resolves.toEqual(john);
await expect(table.findByPrimaryKey({ id: 2 })).resolves.toEqual(amy);
await expect(table.find({ surname: 'Doe', name: 'John' })).resolves.toEqual(john);
await expect(table.find({ surname: 'Doe', name: 'Amy' })).resolves.toEqual(amy);
expect(database.getRecord).not.toHaveBeenCalled(); expect(database.getRecord).not.toHaveBeenCalled();
}); });
it('inserts items', async () => { it('inserts items', () => testInsertItems(records, database, table));
// Arrange. it('deletes items', () => testDeleteItems(records, database, table));
const john = { id: 1, name: 'John', surname: 'Doe' }; it('deletes items by primary key', () => testDeleteItemsByPrimaryKey(records, database, table));
});
describe('CoreDatabaseTable with lazy caching', () => {
let records: User[];
let database: SQLiteDB;
let table: CoreDatabaseTable<User>;
beforeEach(() => [records, database, table] = prepareStubs({ cachingStrategy: CoreDatabaseCachingStrategy.Lazy }));
it('reads no records on initialization', async () => {
await table.initialize(); await table.initialize();
// Act. expect(database.getRecords).not.toHaveBeenCalled();
await table.insert(john); expect(database.getAllRecords).not.toHaveBeenCalled();
// Assert.
expect(database.insertRecord).toHaveBeenCalledWith('users', john);
await expect(table.findByPrimaryKey({ id: 1 })).resolves.toEqual(john);
}); });
it('deletes items', async () => { it('finds items', async () => {
// Arrange. await testFindItems(records, table);
const john = { id: 1, name: 'John', surname: 'Doe' };
const amy = { id: 2, name: 'Amy', surname: 'Doe' };
const jane = { id: 3, name: 'Jane', surname: 'Smith' };
records.push(john); expect(database.getRecord).toHaveBeenCalledTimes(2);
records.push(amy);
records.push(jane);
await table.initialize();
// Act.
await table.delete({ surname: 'Doe' });
// Assert.
expect(database.deleteRecords).toHaveBeenCalledWith('users', { surname: 'Doe' });
await expect(table.findByPrimaryKey({ id: 1 })).rejects.toThrow();
await expect(table.findByPrimaryKey({ id: 2 })).rejects.toThrow();
await expect(table.findByPrimaryKey({ id: 3 })).resolves.toEqual(jane);
}); });
it('deletes items by primary key', async () => { it('inserts items', () => testInsertItems(records, database, table));
// Arrange. it('deletes items', () => testDeleteItems(records, database, table));
const john = { id: 1, name: 'John', surname: 'Doe' }; it('deletes items by primary key', () => testDeleteItemsByPrimaryKey(records, database, table));
const amy = { id: 2, name: 'Amy', surname: 'Doe' };
records.push(john);
records.push(amy);
await table.initialize();
// Act.
await table.deleteByPrimaryKey({ id: 1 });
// Assert.
expect(database.deleteRecords).toHaveBeenCalledWith('users', { id: 1 });
await expect(table.findByPrimaryKey({ id: 1 })).rejects.toThrow();
await expect(table.findByPrimaryKey({ id: 2 })).resolves.toEqual(amy);
});
}); });
@ -163,7 +199,7 @@ describe('CoreDatabaseTable with no caching', () => {
let records: User[]; let records: User[];
let database: SQLiteDB; let database: SQLiteDB;
let table: CoreDatabaseTableProxy<User>; let table: CoreDatabaseTable<User>;
beforeEach(() => [records, database, table] = prepareStubs({ cachingStrategy: CoreDatabaseCachingStrategy.None })); beforeEach(() => [records, database, table] = prepareStubs({ cachingStrategy: CoreDatabaseCachingStrategy.None }));
@ -175,78 +211,13 @@ describe('CoreDatabaseTable with no caching', () => {
}); });
it('finds items', async () => { it('finds items', async () => {
const john = { id: 1, name: 'John', surname: 'Doe' }; await testFindItems(records, table);
const amy = { id: 2, name: 'Amy', surname: 'Doe' };
records.push(john);
records.push(amy);
await table.initialize();
await expect(table.findByPrimaryKey({ id: 1 })).resolves.toEqual(john);
await expect(table.findByPrimaryKey({ id: 2 })).resolves.toEqual(amy);
await expect(table.find({ surname: 'Doe', name: 'John' })).resolves.toEqual(john);
await expect(table.find({ surname: 'Doe', name: 'Amy' })).resolves.toEqual(amy);
expect(database.getRecord).toHaveBeenCalledTimes(4); expect(database.getRecord).toHaveBeenCalledTimes(4);
}); });
it('inserts items', async () => { it('inserts items', () => testInsertItems(records, database, table));
// Arrange. it('deletes items', () => testDeleteItems(records, database, table));
const john = { id: 1, name: 'John', surname: 'Doe' }; it('deletes items by primary key', () => testDeleteItemsByPrimaryKey(records, database, table));
await table.initialize();
// Act.
await table.insert(john);
// Assert.
expect(database.insertRecord).toHaveBeenCalledWith('users', john);
await expect(table.findByPrimaryKey({ id: 1 })).resolves.toEqual(john);
});
it('deletes items', async () => {
// Arrange.
const john = { id: 1, name: 'John', surname: 'Doe' };
const amy = { id: 2, name: 'Amy', surname: 'Doe' };
const jane = { id: 3, name: 'Jane', surname: 'Smith' };
records.push(john);
records.push(amy);
records.push(jane);
await table.initialize();
// Act.
await table.delete({ surname: 'Doe' });
// Assert.
expect(database.deleteRecords).toHaveBeenCalledWith('users', { surname: 'Doe' });
await expect(table.findByPrimaryKey({ id: 1 })).rejects.toThrow();
await expect(table.findByPrimaryKey({ id: 2 })).rejects.toThrow();
await expect(table.findByPrimaryKey({ id: 3 })).resolves.toEqual(jane);
});
it('deletes items by primary key', async () => {
// Arrange.
const john = { id: 1, name: 'John', surname: 'Doe' };
const amy = { id: 2, name: 'Amy', surname: 'Doe' };
records.push(john);
records.push(amy);
await table.initialize();
// Act.
await table.deleteByPrimaryKey({ id: 1 });
// Assert.
expect(database.deleteRecords).toHaveBeenCalledWith('users', { id: 1 });
await expect(table.findByPrimaryKey({ id: 1 })).rejects.toThrow();
await expect(table.findByPrimaryKey({ id: 2 })).resolves.toEqual(amy);
});
}); });

View File

@ -48,6 +48,9 @@ import {
} from '@services/database/filepool'; } from '@services/database/filepool';
import { CoreFileHelper } from './file-helper'; import { CoreFileHelper } from './file-helper';
import { CoreUrl } from '@singletons/url'; import { CoreUrl } from '@singletons/url';
import { CorePromisedValue } from '@classes/promised-value';
import { CoreDatabaseTable } from '@classes/database/database-table';
import { CoreDatabaseCachingStrategy, CoreDatabaseTableProxy } from '@classes/database/database-table-proxy';
/* /*
* Factory for handling downloading files and retrieve downloaded files. * Factory for handling downloading files and retrieve downloaded files.
@ -72,9 +75,13 @@ export class CoreFilepoolProvider {
protected static readonly ERR_FS_OR_NETWORK_UNAVAILABLE = 'CoreFilepoolError:ERR_FS_OR_NETWORK_UNAVAILABLE'; 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'; protected static readonly ERR_QUEUE_ON_PAUSE = 'CoreFilepoolError:ERR_QUEUE_ON_PAUSE';
protected static readonly FILE_UPDATE_UNKNOWN_WHERE_CLAUSE = protected static readonly FILE_IS_UNKNOWN_SQL =
'isexternalfile = 1 OR ((revision IS NULL OR revision = 0) AND (timemodified IS NULL OR timemodified = 0))'; 'isexternalfile = 1 OR ((revision IS NULL OR revision = 0) AND (timemodified IS NULL OR timemodified = 0))';
protected static readonly FILE_IS_UNKNOWN_JS =
({ isexternalfile, revision, timemodified }: CoreFilepoolFileEntry): boolean =>
isexternalfile === 1 || ((revision === null || revision === 0) && (timemodified === null || timemodified === 0));
protected logger: CoreLogger; protected logger: CoreLogger;
protected queueState = CoreFilepoolProvider.QUEUE_PAUSED; protected queueState = CoreFilepoolProvider.QUEUE_PAUSED;
protected urlAttributes: RegExp[] = [ protected urlAttributes: RegExp[] = [
@ -94,6 +101,7 @@ export class CoreFilepoolProvider {
// Variables for DB. // Variables for DB.
protected appDB: Promise<SQLiteDB>; protected appDB: Promise<SQLiteDB>;
protected resolveAppDB!: (appDB: SQLiteDB) => void; protected resolveAppDB!: (appDB: SQLiteDB) => void;
protected filesTables: Record<string, CorePromisedValue<CoreDatabaseTable<CoreFilepoolFileEntry, 'fileId'>>> = {};
constructor() { constructor() {
this.appDB = new Promise(resolve => this.resolveAppDB = resolve); this.appDB = new Promise(resolve => this.resolveAppDB = resolve);
@ -114,6 +122,18 @@ export class CoreFilepoolProvider {
NgZone.run(() => this.checkQueueProcessing()); NgZone.run(() => this.checkQueueProcessing());
}); });
}); });
CoreEvents.on(CoreEvents.SITE_DELETED, async ({ siteId }) => {
if (!siteId || !(siteId in this.filesTables)) {
return;
}
const filesTable = await this.filesTables[siteId];
delete this.filesTables[siteId];
await filesTable.destroy();
});
} }
/** /**
@ -129,6 +149,33 @@ export class CoreFilepoolProvider {
this.resolveAppDB(CoreApp.getDB()); this.resolveAppDB(CoreApp.getDB());
} }
/**
* Get files table.
*
* @param siteId Site id.
* @returns Files table.
*/
async getFilesTable(siteId?: string): Promise<CoreDatabaseTable<CoreFilepoolFileEntry, 'fileId'>> {
siteId = siteId ?? CoreSites.getCurrentSiteId();
if (!(siteId in this.filesTables)) {
const filesTable = this.filesTables[siteId] = new CorePromisedValue();
const database = await CoreSites.getSiteDb(siteId);
const table = new CoreDatabaseTableProxy<CoreFilepoolFileEntry, 'fileId'>(
{ cachingStrategy: CoreDatabaseCachingStrategy.Lazy },
database,
FILES_TABLE_NAME,
['fileId'],
);
await table.initialize();
filesTable.resolve(table);
}
return this.filesTables[siteId];
}
/** /**
* Link a file with a component. * Link a file with a component.
* *
@ -215,9 +262,9 @@ export class CoreFilepoolProvider {
...data, ...data,
}; };
const db = await CoreSites.getSiteDb(siteId); const filesTable = await this.getFilesTable(siteId);
await db.insertRecord(FILES_TABLE_NAME, record); await filesTable.insert(record);
} }
/** /**
@ -558,13 +605,14 @@ export class CoreFilepoolProvider {
*/ */
async clearFilepool(siteId: string): Promise<void> { async clearFilepool(siteId: string): Promise<void> {
const db = await CoreSites.getSiteDb(siteId); const db = await CoreSites.getSiteDb(siteId);
const filesTable = await this.getFilesTable(siteId);
// Read the data first to be able to notify the deletions. // Read the data first to be able to notify the deletions.
const filesEntries = await db.getAllRecords<CoreFilepoolFileEntry>(FILES_TABLE_NAME); const filesEntries = await filesTable.all();
const filesLinks = await db.getAllRecords<CoreFilepoolLinksRecord>(LINKS_TABLE_NAME); const filesLinks = await db.getAllRecords<CoreFilepoolLinksRecord>(LINKS_TABLE_NAME);
await Promise.all([ await Promise.all([
db.deleteRecords(FILES_TABLE_NAME), filesTable.delete(),
db.deleteRecords(LINKS_TABLE_NAME), db.deleteRecords(LINKS_TABLE_NAME),
]); ]);
@ -1119,13 +1167,14 @@ export class CoreFilepoolProvider {
} }
const db = await CoreSites.getSiteDb(siteId); const db = await CoreSites.getSiteDb(siteId);
const filesTable = await this.getFilesTable(siteId);
const extension = CoreMimetypeUtils.getFileExtension(entry.path); const extension = CoreMimetypeUtils.getFileExtension(entry.path);
if (!extension) { if (!extension) {
// Files does not have extension. Invalidate file (stale = true). // Files does not have extension. Invalidate file (stale = true).
// Minor problem: file will remain in the filesystem once downloaded again. // Minor problem: file will remain in the filesystem once downloaded again.
this.logger.debug('Staled file with no extension ' + entry.fileId); this.logger.debug('Staled file with no extension ' + entry.fileId);
await db.updateRecords(FILES_TABLE_NAME, { stale: 1 }, { fileId: entry.fileId }); await filesTable.update({ stale: 1 }, { fileId: entry.fileId });
return; return;
} }
@ -1135,7 +1184,7 @@ export class CoreFilepoolProvider {
entry.fileId = CoreMimetypeUtils.removeExtension(fileId); entry.fileId = CoreMimetypeUtils.removeExtension(fileId);
entry.extension = extension; entry.extension = extension;
await db.updateRecords(FILES_TABLE_NAME, entry, { fileId }); await filesTable.update(entry, { fileId });
if (entry.fileId == fileId) { if (entry.fileId == fileId) {
// File ID hasn't changed, we're done. // File ID hasn't changed, we're done.
this.logger.debug('Removed extesion ' + extension + ' from file ' + entry.fileId); this.logger.debug('Removed extesion ' + extension + ' from file ' + entry.fileId);
@ -1396,15 +1445,13 @@ export class CoreFilepoolProvider {
*/ */
async getFilesByComponent(siteId: string, component: string, componentId?: string | number): Promise<CoreFilepoolFileEntry[]> { async getFilesByComponent(siteId: string, component: string, componentId?: string | number): Promise<CoreFilepoolFileEntry[]> {
const db = await CoreSites.getSiteDb(siteId); const db = await CoreSites.getSiteDb(siteId);
const filesTable = await this.getFilesTable(siteId);
const items = await this.getComponentFiles(db, component, componentId); const items = await this.getComponentFiles(db, component, componentId);
const files: CoreFilepoolFileEntry[] = []; const files: CoreFilepoolFileEntry[] = [];
await Promise.all(items.map(async (item) => { await Promise.all(items.map(async (item) => {
try { try {
const fileEntry = await db.getRecord<CoreFilepoolFileEntry>( const fileEntry = await filesTable.findByPrimaryKey({ fileId: item.fileId });
FILES_TABLE_NAME,
{ fileId: item.fileId },
);
if (!fileEntry) { if (!fileEntry) {
return; return;
@ -2137,14 +2184,9 @@ export class CoreFilepoolProvider {
* @return Resolved with file object from DB on success, rejected otherwise. * @return Resolved with file object from DB on success, rejected otherwise.
*/ */
protected async hasFileInPool(siteId: string, fileId: string): Promise<CoreFilepoolFileEntry> { protected async hasFileInPool(siteId: string, fileId: string): Promise<CoreFilepoolFileEntry> {
const db = await CoreSites.getSiteDb(siteId); const filesTable = await this.getFilesTable(siteId);
const entry = await db.getRecord<CoreFilepoolFileEntry>(FILES_TABLE_NAME, { fileId });
if (entry === undefined) { return filesTable.findByPrimaryKey({ fileId });
throw new CoreError('File not found in filepool.');
}
return entry;
} }
/** /**
@ -2176,11 +2218,17 @@ export class CoreFilepoolProvider {
* @return Resolved on success. * @return Resolved on success.
*/ */
async invalidateAllFiles(siteId: string, onlyUnknown: boolean = true): Promise<void> { async invalidateAllFiles(siteId: string, onlyUnknown: boolean = true): Promise<void> {
const db = await CoreSites.getSiteDb(siteId); const filesTable = await this.getFilesTable(siteId);
const where = onlyUnknown ? CoreFilepoolProvider.FILE_UPDATE_UNKNOWN_WHERE_CLAUSE : undefined; onlyUnknown
? await filesTable.updateWhere(
await db.updateRecordsWhere(FILES_TABLE_NAME, { stale: 1 }, where); { stale: 1 },
{
sql: CoreFilepoolProvider.FILE_IS_UNKNOWN_SQL,
js: CoreFilepoolProvider.FILE_IS_UNKNOWN_JS,
},
)
: await filesTable.update({ stale: 1 });
} }
/** /**
@ -2199,9 +2247,9 @@ export class CoreFilepoolProvider {
const file = await this.fixPluginfileURL(siteId, fileUrl); const file = await this.fixPluginfileURL(siteId, fileUrl);
const fileId = this.getFileIdByUrl(CoreFileHelper.getFileUrl(file)); const fileId = this.getFileIdByUrl(CoreFileHelper.getFileUrl(file));
const db = await CoreSites.getSiteDb(siteId); const filesTable = await this.getFilesTable(siteId);
await db.updateRecords(FILES_TABLE_NAME, { stale: 1 }, { fileId }); await filesTable.update({ stale: 1 }, { fileId });
} }
/** /**
@ -2221,7 +2269,7 @@ export class CoreFilepoolProvider {
onlyUnknown: boolean = true, onlyUnknown: boolean = true,
): Promise<void> { ): Promise<void> {
const db = await CoreSites.getSiteDb(siteId); const db = await CoreSites.getSiteDb(siteId);
const filesTable = await this.getFilesTable(siteId);
const items = await this.getComponentFiles(db, component, componentId); const items = await this.getComponentFiles(db, component, componentId);
if (!items.length) { if (!items.length) {
@ -2236,10 +2284,19 @@ export class CoreFilepoolProvider {
whereAndParams.sql = 'fileId ' + whereAndParams.sql; whereAndParams.sql = 'fileId ' + whereAndParams.sql;
if (onlyUnknown) { if (onlyUnknown) {
whereAndParams.sql += ' AND (' + CoreFilepoolProvider.FILE_UPDATE_UNKNOWN_WHERE_CLAUSE + ')'; whereAndParams.sql += ' AND (' + CoreFilepoolProvider.FILE_IS_UNKNOWN_SQL + ')';
} }
await db.updateRecordsWhere(FILES_TABLE_NAME, { stale: 1 }, whereAndParams.sql, whereAndParams.params); await filesTable.updateWhere(
{ stale: 1 },
{
sql: whereAndParams.sql,
sqlParams: whereAndParams.params,
js: record => fileIds.includes(record.fileId) && (
!onlyUnknown || CoreFilepoolProvider.FILE_IS_UNKNOWN_JS(record)
),
},
);
} }
/** /**
@ -2657,6 +2714,8 @@ export class CoreFilepoolProvider {
*/ */
protected async removeFileById(siteId: string, fileId: string): Promise<void> { protected async removeFileById(siteId: string, fileId: string): Promise<void> {
const db = await CoreSites.getSiteDb(siteId); const db = await CoreSites.getSiteDb(siteId);
const filesTable = await this.getFilesTable(siteId);
// Get the path to the file first since it relies on the file object stored in the pool. // 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. // Don't use getFilePath to prevent performing 2 DB requests.
let path = this.getFilepoolFolderPath(siteId) + '/' + fileId; let path = this.getFilepoolFolderPath(siteId) + '/' + fileId;
@ -2682,7 +2741,7 @@ export class CoreFilepoolProvider {
const promises: Promise<unknown>[] = []; const promises: Promise<unknown>[] = [];
// Remove entry from filepool store. // Remove entry from filepool store.
promises.push(db.deleteRecords(FILES_TABLE_NAME, conditions)); promises.push(filesTable.delete(conditions));
// Remove links. // Remove links.
promises.push(db.deleteRecords(LINKS_TABLE_NAME, conditions)); promises.push(db.deleteRecords(LINKS_TABLE_NAME, conditions));