From 92f574235197ccfbf6cc092f82478f265ed653cb Mon Sep 17 00:00:00 2001 From: Dani Palou Date: Wed, 10 Mar 2021 15:58:10 +0100 Subject: [PATCH] MOBILE-3630 sharefiles: Implement services --- src/app/app.component.ts | 2 +- src/core/features/compile/services/compile.ts | 4 +- src/core/features/features.module.ts | 2 + src/core/features/sharedfiles/lang.json | 11 + .../services/database/sharedfiles.ts | 43 +++ .../sharedfiles/services/handlers/settings.ts | 56 ++++ .../sharedfiles/services/handlers/upload.ts | 74 +++++ .../services/sharedfiles-helper.ts | 277 ++++++++++++++++++ .../sharedfiles/services/sharedfiles.ts | 269 +++++++++++++++++ .../sharedfiles/sharedfiles.module.ts | 64 ++++ src/core/singletons/events.ts | 17 ++ 11 files changed, 816 insertions(+), 3 deletions(-) create mode 100644 src/core/features/sharedfiles/lang.json create mode 100644 src/core/features/sharedfiles/services/database/sharedfiles.ts create mode 100644 src/core/features/sharedfiles/services/handlers/settings.ts create mode 100644 src/core/features/sharedfiles/services/handlers/upload.ts create mode 100644 src/core/features/sharedfiles/services/sharedfiles-helper.ts create mode 100644 src/core/features/sharedfiles/services/sharedfiles.ts create mode 100644 src/core/features/sharedfiles/sharedfiles.module.ts diff --git a/src/app/app.component.ts b/src/app/app.component.ts index c88c155cc..d81f14598 100644 --- a/src/app/app.component.ts +++ b/src/app/app.component.ts @@ -155,7 +155,7 @@ export class AppComponent implements OnInit, AfterViewInit { this.lastUrls[url] = Date.now(); - CoreEvents.trigger(CoreEvents.APP_LAUNCHED_URL, url); + CoreEvents.trigger(CoreEvents.APP_LAUNCHED_URL, { url }); CoreCustomURLSchemes.handleCustomURL(url).catch((error) => { CoreCustomURLSchemes.treatHandleCustomURLError(error); }); diff --git a/src/core/features/compile/services/compile.ts b/src/core/features/compile/services/compile.ts index 44b1ac0c2..c4b0aed05 100644 --- a/src/core/features/compile/services/compile.ts +++ b/src/core/features/compile/services/compile.ts @@ -57,7 +57,7 @@ import { CORE_LOGIN_SERVICES } from '@features/login/login.module'; import { CORE_MAINMENU_SERVICES } from '@features/mainmenu/mainmenu.module'; import { CORE_PUSHNOTIFICATIONS_SERVICES } from '@features/pushnotifications/pushnotifications.module'; import { CORE_QUESTION_SERVICES } from '@features/question/question.module'; -// @todo import { CORE_SHAREDFILES_SERVICES } from '@features/sharedfiles/sharedfiles.module'; +import { CORE_SHAREDFILES_SERVICES } from '@features/sharedfiles/sharedfiles.module'; import { CORE_RATING_SERVICES } from '@features/rating/rating.module'; import { CORE_SEARCH_SERVICES } from '@features/search/search.module'; import { CORE_SETTINGS_SERVICES } from '@features/settings/settings.module'; @@ -271,7 +271,7 @@ export class CoreCompileProvider { ...CORE_RATING_SERVICES, ...CORE_SEARCH_SERVICES, ...CORE_SETTINGS_SERVICES, - // @todo ...CORE_SHAREDFILES_SERVICES, + ...CORE_SHAREDFILES_SERVICES, ...CORE_SITEHOME_SERVICES, CoreSitePluginsProvider, ...CORE_TAG_SERVICES, diff --git a/src/core/features/features.module.ts b/src/core/features/features.module.ts index 9be217cb1..adbc3c687 100644 --- a/src/core/features/features.module.ts +++ b/src/core/features/features.module.ts @@ -33,6 +33,7 @@ import { CoreSearchModule } from './search/search.module'; import { CoreCommentsModule } from './comments/comments.module'; import { CoreSitePluginsModule } from './siteplugins/siteplugins.module'; import { CoreRatingModule } from './rating/rating.module'; +import { CoreSharedFilesModule } from './sharedfiles/sharedfiles.module'; @NgModule({ imports: [ @@ -55,6 +56,7 @@ import { CoreRatingModule } from './rating/rating.module'; CoreCommentsModule, CoreSitePluginsModule, CoreRatingModule, + CoreSharedFilesModule, ], }) export class CoreFeaturesModule {} diff --git a/src/core/features/sharedfiles/lang.json b/src/core/features/sharedfiles/lang.json new file mode 100644 index 000000000..c7ba93a40 --- /dev/null +++ b/src/core/features/sharedfiles/lang.json @@ -0,0 +1,11 @@ +{ + "chooseaccountstorefile": "Choose an account to store the file in.", + "chooseactionrepeatedfile": "A file with this name already exists. Do you want to replace the existing file or rename it to \"{{$a}}\"?", + "errorreceivefilenosites": "There are no sites stored. Please add a site before sharing a file with the app.", + "nosharedfiles": "There are no shared files stored on this site.", + "nosharedfilestoupload": "You have no files to upload here. If you want to upload a file from another app, locate the file and click the 'Open in' button.", + "rename": "Rename", + "replace": "Replace", + "sharedfiles": "Shared files", + "successstorefile": "File successfully stored. Select the file to upload to your private files or use in an activity." +} \ No newline at end of file diff --git a/src/core/features/sharedfiles/services/database/sharedfiles.ts b/src/core/features/sharedfiles/services/database/sharedfiles.ts new file mode 100644 index 000000000..346fae39f --- /dev/null +++ b/src/core/features/sharedfiles/services/database/sharedfiles.ts @@ -0,0 +1,43 @@ +// (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 { CoreAppSchema } from '@services/app'; + +/** + * Database variables for CoreSharedFilesProvider service. + */ +export const SHARED_FILES_TABLE_NAME = 'shared_files'; +export const APP_SCHEMA: CoreAppSchema = { + name: 'CoreSharedFilesProvider', + version: 1, + tables: [ + { + name: SHARED_FILES_TABLE_NAME, + columns: [ + { + name: 'id', + type: 'TEXT', + primaryKey: true, + }, + ], + }, + ], +}; + +/** + * Data stored in DB for shared files. + */ +export type CoreSharedFilesDBRecord = { + id: string; +}; diff --git a/src/core/features/sharedfiles/services/handlers/settings.ts b/src/core/features/sharedfiles/services/handlers/settings.ts new file mode 100644 index 000000000..66cc467de --- /dev/null +++ b/src/core/features/sharedfiles/services/handlers/settings.ts @@ -0,0 +1,56 @@ +// (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 { CoreSettingsHandler, CoreSettingsHandlerData } from '@features/settings/services/settings-delegate'; +import { SHAREDFILES_PAGE_NAME } from '@features/sharedfiles/sharedfiles.module'; +import { CoreApp } from '@services/app'; +import { makeSingleton } from '@singletons'; + +/** + * Shared files settings handler. + */ +@Injectable({ providedIn: 'root' }) +export class CoreSharedFilesSettingsHandlerService implements CoreSettingsHandler { + + name = 'CoreSharedFiles'; + priority = 200; + + /** + * Check if the handler is enabled on a site level. + * + * @return Whether or not the handler is enabled on a site level. + */ + async isEnabled(): Promise { + return CoreApp.isIOS(); + } + + /** + * Returns the data needed to render the handler. + * + * @return Data needed to render the handler. + */ + getDisplayData(): CoreSettingsHandlerData { + return { + icon: 'fas-folder', + title: 'core.sharedfiles.sharedfiles', + page: SHAREDFILES_PAGE_NAME + '/list/root', + params: { manage: true, hideSitePicker: true }, + class: 'core-sharedfiles-settings-handler', + }; + } + +} + +export const CoreSharedFilesSettingsHandler = makeSingleton(CoreSharedFilesSettingsHandlerService); diff --git a/src/core/features/sharedfiles/services/handlers/upload.ts b/src/core/features/sharedfiles/services/handlers/upload.ts new file mode 100644 index 000000000..9ee934de3 --- /dev/null +++ b/src/core/features/sharedfiles/services/handlers/upload.ts @@ -0,0 +1,74 @@ +// (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 { + CoreFileUploaderHandler, + CoreFileUploaderHandlerData, + CoreFileUploaderHandlerResult, +} from '@features/fileuploader/services/fileuploader-delegate'; +import { CoreApp } from '@services/app'; +import { makeSingleton } from '@singletons'; +import { CoreSharedFilesHelper } from '../sharedfiles-helper'; +/** + * Handler to upload files from the album. + */ +@Injectable({ providedIn: 'root' }) +export class CoreSharedFilesUploadHandlerService implements CoreFileUploaderHandler { + + name = 'CoreSharedFilesUpload'; + priority = 1300; + + /** + * Whether or not the handler is enabled on a site level. + * + * @return True or promise resolved with true if enabled. + */ + async isEnabled(): Promise { + return CoreApp.isIOS(); + } + + /** + * Given a list of mimetypes, return the ones that are supported by the handler. + * + * @param mimetypes List of mimetypes. + * @return Supported mimetypes. + */ + getSupportedMimetypes(mimetypes: string[]): string[] { + return mimetypes; + } + + /** + * Get the data to display the handler. + * + * @return Data. + */ + getData(): CoreFileUploaderHandlerData { + return { + title: 'core.sharedfiles.sharedfiles', + class: 'core-sharedfiles-fileuploader-handler', + icon: 'folder', + action: ( + maxSize?: number, + upload?: boolean, + allowOffline?: boolean, + mimetypes?: string[], + ): Promise => CoreSharedFilesHelper.pickSharedFile(mimetypes), + }; + } + +} + +export const CoreSharedFilesUploadHandler = makeSingleton(CoreSharedFilesUploadHandlerService); diff --git a/src/core/features/sharedfiles/services/sharedfiles-helper.ts b/src/core/features/sharedfiles/services/sharedfiles-helper.ts new file mode 100644 index 000000000..704c0fb9a --- /dev/null +++ b/src/core/features/sharedfiles/services/sharedfiles-helper.ts @@ -0,0 +1,277 @@ +// (C) Copyright 2015 Moodle Pty Ltd. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import { Injectable } from '@angular/core'; +import { FileEntry } from '@ionic-native/file'; + +import { CoreCanceledError } from '@classes/errors/cancelederror'; +import { CoreFileUploader } from '@features/fileuploader/services/fileuploader'; +import { CoreFileUploaderHandlerResult } from '@features/fileuploader/services/fileuploader-delegate'; +import { CoreApp } from '@services/app'; +import { CoreFile } from '@services/file'; +import { CoreNavigator } from '@services/navigator'; +import { CoreSites } from '@services/sites'; +import { CoreDomUtils } from '@services/utils/dom'; +import { AlertController, ApplicationInit, makeSingleton, ModalController, Platform, Translate } from '@singletons'; +import { CoreEvents } from '@singletons/events'; +import { CoreLogger } from '@singletons/logger'; +import { CoreSharedFilesListModalComponent } from '../components/list-modal/list-modal'; +import { CoreSharedFiles } from './sharedfiles'; +import { SHAREDFILES_PAGE_NAME } from '../sharedfiles.module'; +import { CoreSharedFilesChooseSitePage } from '../pages/choose-site/choose-site'; +import { CoreError } from '@classes/errors/error'; + +/** + * Helper service to share files with the app. + */ +@Injectable({ providedIn: 'root' }) +export class CoreSharedFilesHelperProvider { + + protected logger: CoreLogger; + + constructor() { + this.logger = CoreLogger.getInstance('CoreSharedFilesHelperProvider'); + } + + /** + * Initialize. + */ + initialize(): void { + if (!CoreApp.isIOS()) { + return; + } + + let lastCheck = Date.now(); + + // Check if there are new files at app start and when the app is resumed. + this.searchIOSNewSharedFiles(); + + Platform.resume.subscribe(() => { + // Wait a bit to make sure that APP_LAUNCHED_URL is treated before this callback. + setTimeout(() => { + if (Date.now() - lastCheck < 1000) { + // Last check less than 1s ago, don't do anything. + return; + } + + lastCheck = Date.now(); + this.searchIOSNewSharedFiles(); + }, 200); + }); + + CoreEvents.on(CoreEvents.APP_LAUNCHED_URL, (data) => { + if (data.url.indexOf('file://') === 0) { + // We received a file in iOS, it's probably a shared file. Treat it. + lastCheck = Date.now(); + this.searchIOSNewSharedFiles(data.url); + } + }); + } + + /** + * Ask a user if he wants to replace a file (using originalName) or rename it (using newName). + * + * @param originalName Original name. + * @param newName New name. + * @return Promise resolved with the name to use when the user chooses. Rejected if user cancels. + */ + async askRenameReplace(originalName: string, newName: string): Promise { + const alert = await AlertController.create({ + header: Translate.instant('core.sharedfiles.sharedfiles'), + message: Translate.instant('core.sharedfiles.chooseactionrepeatedfile', { $a: newName }), + buttons: [ + { + text: Translate.instant('core.sharedfiles.rename'), + role: 'rename', + }, + { + text: Translate.instant('core.sharedfiles.replace'), + role: 'replace', + }, + ], + }); + + await alert.present(); + + const result = await alert.onDidDismiss(); + + if (result.role == 'rename') { + return newName; + } else if (result.role == 'replace') { + return originalName; + } else { + // Canceled. + throw new CoreCanceledError(); + } + } + + /** + * Go to the choose site view. + * + * @param filePath File path to send to the view. + * @param isInbox Whether the file is in the Inbox folder. + */ + goToChooseSite(filePath: string, isInbox?: boolean): void { + if (CoreSites.isLoggedIn()) { + CoreNavigator.navigateToSitePath(`/${SHAREDFILES_PAGE_NAME}/choosesite`, { + params: { filePath, isInbox }, + }); + } else { + CoreNavigator.navigate(`/${SHAREDFILES_PAGE_NAME}/choosesite`, { + params: { filePath, isInbox }, + }); + } + } + + /** + * Whether the user is already choosing a site to store a shared file. + * + * @return Whether the user is already choosing a site to store a shared file. + */ + protected isChoosingSite(): boolean { + return CoreNavigator.getCurrentRoute({ pageComponent: CoreSharedFilesChooseSitePage }) !== null; + } + + /** + * Open the view to select a shared file. + * + * @param mimetypes List of supported mimetypes. If undefined, all mimetypes supported. + * @return Promise resolved when a file is picked, rejected if file picker is closed without selecting a file. + */ + async pickSharedFile(mimetypes?: string[]): Promise { + const modal = await ModalController.create({ + component: CoreSharedFilesListModalComponent, + cssClass: 'core-modal-fullscreen', + componentProps: { mimetypes, pick: true }, + }); + + await modal.present(); + + const result = await modal.onDidDismiss(); + const file: FileEntry | undefined = result.data; + + if (!file) { + // User cancelled. + throw new CoreCanceledError(); + } + + const error = CoreFileUploader.isInvalidMimetype(mimetypes, file.fullPath); + if (error) { + throw new CoreError(error); + } + + return { + path: file.fullPath, + treated: false, + }; + } + + /** + * Delete a shared file. + * + * @param fileEntry The file entry to delete. + * @param isInbox Whether the file is in the Inbox folder. + * @return Promise resolved when done. + */ + protected removeSharedFile(fileEntry: FileEntry, isInbox?: boolean): Promise { + if (isInbox) { + return CoreSharedFiles.deleteInboxFile(fileEntry); + } else { + return CoreFile.removeFileByFileEntry(fileEntry); + } + } + + /** + * Checks if there is a new file received in iOS and move it to the shared folder of current site. + * If more than one site is found, the user will have to choose the site where to store it in. + * If more than one file is found, treat only the first one. + * + * @param path Path to a file received when launching the app. + * @return Promise resolved when done. + */ + async searchIOSNewSharedFiles(path?: string): Promise { + try { + await ApplicationInit.donePromise; + + if (this.isChoosingSite()) { + // We're already treating a shared file. Abort. + return; + } + + let fileEntry: FileEntry | undefined; + if (path) { + // The app was launched with the path to the file, get the file. + fileEntry = await CoreFile.getExternalFile(path); + } else { + // No path received, search if there is any file in the Inbox folder. + fileEntry = await CoreSharedFiles.checkIOSNewFiles(); + } + + if (!fileEntry) { + return; + } + + const siteIds = await CoreSites.getSitesIds(); + + if (!siteIds.length) { + // No sites stored, show error and delete the file. + CoreDomUtils.showErrorModal('core.sharedfiles.errorreceivefilenosites', true); + + return this.removeSharedFile(fileEntry, !path); + } else if (siteIds.length == 1) { + return this.storeSharedFileInSite(fileEntry, siteIds[0], !path); + } else if (!this.isChoosingSite()) { + this.goToChooseSite(fileEntry.toURL(), !path); + } + } catch (error) { + if (error) { + this.logger.error('Error searching iOS new shared files', error, path); + } + } + } + + /** + * Store a shared file in a site's shared files folder. + * + * @param fileEntry Shared file entry. + * @param siteId Site ID. If not defined, current site. + * @param isInbox Whether the file is in the Inbox folder. + * @return Promise resolved when done. + */ + async storeSharedFileInSite(fileEntry: FileEntry, siteId?: string, isInbox?: boolean): Promise { + siteId = siteId || CoreSites.getCurrentSiteId(); + + // First of all check if there's already a file with the same name in the shared files folder. + const sharedFilesDirPath = CoreSharedFiles.getSiteSharedFilesDirPath(siteId); + + let newName = await CoreFile.getUniqueNameInFolder(sharedFilesDirPath, fileEntry.name); + + if (newName.toLowerCase() != fileEntry.name.toLowerCase()) { + // Repeated name. Ask the user what he wants to do. + newName = await this.askRenameReplace(fileEntry.name, newName); + } + + try { + await CoreSharedFiles.storeFileInSite(fileEntry, newName, siteId); + } catch (error) { + CoreDomUtils.showErrorModal(error || 'Error moving file.'); + } finally { + this.removeSharedFile(fileEntry, isInbox); + CoreDomUtils.showAlertTranslated('core.success', 'core.sharedfiles.successstorefile'); + } + } + +} + +export const CoreSharedFilesHelper = makeSingleton(CoreSharedFilesHelperProvider); diff --git a/src/core/features/sharedfiles/services/sharedfiles.ts b/src/core/features/sharedfiles/services/sharedfiles.ts new file mode 100644 index 000000000..a774aea78 --- /dev/null +++ b/src/core/features/sharedfiles/services/sharedfiles.ts @@ -0,0 +1,269 @@ +// (C) Copyright 2015 Moodle Pty Ltd. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import { Injectable } from '@angular/core'; +import { FileEntry, DirectoryEntry } from '@ionic-native/file'; +import { Md5 } from 'ts-md5/dist/md5'; + +import { SQLiteDB } from '@classes/sqlitedb'; +import { CoreLogger } from '@singletons/logger'; +import { CoreApp } from '@services/app'; +import { CoreFile } from '@services/file'; +import { CoreUtils } from '@services/utils/utils'; +import { CoreTextUtils } from '@services/utils/text'; +import { CoreMimetypeUtils } from '@services/utils/mimetype'; +import { CoreSites } from '@services/sites'; +import { CoreEvents } from '@singletons/events'; +import { makeSingleton } from '@singletons'; +import { APP_SCHEMA, CoreSharedFilesDBRecord, SHARED_FILES_TABLE_NAME } from './database/sharedfiles'; + +/** + * Service to share files with the app. + */ +@Injectable({ providedIn: 'root' }) +export class CoreSharedFilesProvider { + + static readonly SHARED_FILES_FOLDER = 'sharedfiles'; + + protected logger: CoreLogger; + // Variables for DB. + protected appDB: Promise; + protected resolveAppDB!: (appDB: SQLiteDB) => void; + + constructor() { + this.logger = CoreLogger.getInstance('CoreSharedFilesProvider'); + this.appDB = new Promise(resolve => this.resolveAppDB = resolve); + } + + /** + * Initialize database. + * + * @return Promise resolved when done. + */ + async initializeDatabase(): Promise { + try { + await CoreApp.createTablesFromSchema(APP_SCHEMA); + } catch (e) { + // Ignore errors. + } + + this.resolveAppDB(CoreApp.getDB()); + } + + /** + * Checks if there is a new file received in iOS. If more than one file is found, treat only the first one. + * The file returned is marked as "treated" and will be deleted in the next execution. + * + * @return Promise resolved with a new file to be treated. If no new files found, resolved with undefined. + */ + async checkIOSNewFiles(): Promise { + this.logger.debug('Search for new files on iOS'); + + const entries = await CoreUtils.ignoreErrors(CoreFile.getDirectoryContents('Inbox')); + + if (!entries || !entries.length) { + return; + } + + let fileToReturn: FileEntry | undefined; + + for (let i = 0; i < entries.length; i++) { + if (entries[i].isDirectory) { + continue; + } + + const fileEntry = entries[i]; + const fileId = this.getFileId(fileEntry); + + try { + // Check if file was already treated. + await this.isFileTreated(fileId); + + // File already treated, delete it. No need to block the execution for this. + this.deleteInboxFile(fileEntry); + } catch { + // File not treated before. + this.logger.debug(`Found new file ${fileEntry.name} shared with the app.`); + fileToReturn = fileEntry; + break; + } + } + + if (!fileToReturn) { + return; + } + + // Mark it as "treated". + const fileId = this.getFileId(fileToReturn); + + await this.markAsTreated(fileId); + + this.logger.debug(`File marked as "treated": ${fileToReturn.name}`); + + return fileToReturn; + } + + /** + * Deletes a file in the Inbox folder (shared with the app). + * + * @param entry FileEntry. + * @return Promise resolved when done, rejected otherwise. + */ + async deleteInboxFile(entry: FileEntry): Promise { + this.logger.debug('Delete inbox file: ' + entry.name); + + await CoreUtils.ignoreErrors(CoreFile.removeFileByFileEntry(entry)); + + try { + await this.unmarkAsTreated(this.getFileId(entry)); + + this.logger.debug(`"Treated" mark removed from file: ${entry.name}`); + } catch (error) { + this.logger.debug(`Error deleting "treated" mark from file: ${entry.name}`, error); + + throw error; + } + } + + /** + * Get the ID of a file for managing "treated" files. + * + * @param entry FileEntry. + * @return File ID. + */ + protected getFileId(entry: FileEntry): string { + return Md5.hashAsciiStr(entry.name); + } + + /** + * Get the shared files stored in a site. + * + * @param siteId Site ID. If not defined, current site. + * @param path Path to search inside the site shared folder. + * @param mimetypes List of supported mimetypes. If undefined, all mimetypes supported. + * @return Promise resolved with the files. + */ + async getSiteSharedFiles(siteId?: string, path?: string, mimetypes?: string[]): Promise<(FileEntry | DirectoryEntry)[]> { + let pathToGet = this.getSiteSharedFilesDirPath(siteId); + if (path) { + pathToGet = CoreTextUtils.concatenatePaths(pathToGet, path); + } + + try { + let entries = await CoreFile.getDirectoryContents(pathToGet); + + if (mimetypes) { + // Get only files with the right mimetype and the ones we cannot determine the mimetype. + entries = entries.filter((entry) => { + const extension = CoreMimetypeUtils.getFileExtension(entry.name); + const mimetype = CoreMimetypeUtils.getMimeType(extension); + + return !mimetype || mimetypes.indexOf(mimetype) > -1; + }); + } + + return entries; + } catch { + // Directory not found, return empty list. + return []; + } + } + + /** + * Get the path to a site's shared files folder. + * + * @param siteId Site ID. If not defined, current site. + * @return Path. + */ + getSiteSharedFilesDirPath(siteId?: string): string { + siteId = siteId || CoreSites.getCurrentSiteId(); + + return CoreFile.getSiteFolder(siteId) + '/' + CoreSharedFilesProvider.SHARED_FILES_FOLDER; + } + + /** + * Check if a file has been treated already. + * + * @param fileId File ID. + * @return Resolved if treated, rejected otherwise. + */ + protected async isFileTreated(fileId: string): Promise { + const db = await this.appDB; + + return db.getRecord(SHARED_FILES_TABLE_NAME, { id: fileId }); + } + + /** + * Mark a file as treated. + * + * @param fileId File ID. + * @return Promise resolved when marked. + */ + protected async markAsTreated(fileId: string): Promise { + try { + // Check if it's already marked. + await this.isFileTreated(fileId); + } catch (err) { + // Doesn't exist, insert it. + const db = await this.appDB; + + await db.insertRecord(SHARED_FILES_TABLE_NAME, { id: fileId }); + } + } + + /** + * Store a file in a site's shared folder. + * + * @param entry File entry. + * @param newName Name of the new file. If not defined, use original file's name. + * @param siteId Site ID. If not defined, current site. + * @return Promise resolved when done. + */ + async storeFileInSite(entry: FileEntry, newName?: string, siteId?: string): Promise { + siteId = siteId || CoreSites.getCurrentSiteId(); + + if (!entry || !siteId) { + return; + } + + newName = newName || entry.name; + + const sharedFilesFolder = this.getSiteSharedFilesDirPath(siteId); + const newPath = CoreTextUtils.concatenatePaths(sharedFilesFolder, newName); + + // Create dir if it doesn't exist already. + await CoreFile.createDir(sharedFilesFolder); + + const newFile = await CoreFile.moveExternalFile(entry.toURL(), newPath); + + CoreEvents.trigger(CoreEvents.FILE_SHARED, { siteId, name: newName }); + + return newFile; + } + + /** + * Unmark a file as treated. + * + * @param fileId File ID. + * @return Resolved when unmarked. + */ + protected async unmarkAsTreated(fileId: string): Promise { + const db = await this.appDB; + + await db.deleteRecords(SHARED_FILES_TABLE_NAME, { id: fileId }); + } + +} + +export const CoreSharedFiles = makeSingleton(CoreSharedFilesProvider); diff --git a/src/core/features/sharedfiles/sharedfiles.module.ts b/src/core/features/sharedfiles/sharedfiles.module.ts new file mode 100644 index 000000000..5d82b264a --- /dev/null +++ b/src/core/features/sharedfiles/sharedfiles.module.ts @@ -0,0 +1,64 @@ +// (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 { AppRoutingModule } from '@/app/app-routing.module'; +import { APP_INITIALIZER, NgModule, Type } from '@angular/core'; +import { Routes } from '@angular/router'; + +import { CoreFileUploaderDelegate } from '@features/fileuploader/services/fileuploader-delegate'; +import { CoreMainMenuTabRoutingModule } from '@features/mainmenu/mainmenu-tab-routing.module'; +import { CoreSitePreferencesRoutingModule } from '@features/settings/pages/site/site-routing'; +import { CoreSettingsDelegate } from '@features/settings/services/settings-delegate'; +import { CoreSharedFilesComponentsModule } from './components/components.module'; +import { CoreSharedFilesSettingsHandler } from './services/handlers/settings'; +import { CoreSharedFilesUploadHandler } from './services/handlers/upload'; +import { CoreSharedFiles, CoreSharedFilesProvider } from './services/sharedfiles'; +import { CoreSharedFilesHelper, CoreSharedFilesHelperProvider } from './services/sharedfiles-helper'; + +export const CORE_SHAREDFILES_SERVICES: Type[] = [ + CoreSharedFilesProvider, + CoreSharedFilesHelperProvider, +]; + +export const SHAREDFILES_PAGE_NAME = 'sharedfiles'; + +const routes: Routes = [ + { + path: SHAREDFILES_PAGE_NAME, + loadChildren: () => import('./sharedfiles-lazy.module').then(m => m.CoreSharedFilesLazyModule), + }, +]; + +@NgModule({ + imports: [ + AppRoutingModule.forChild(routes), + CoreMainMenuTabRoutingModule.forChild(routes), + CoreSitePreferencesRoutingModule.forChild(routes), + CoreSharedFilesComponentsModule, + ], + providers: [ + { + provide: APP_INITIALIZER, + multi: true, + useFactory: () => async () => { + CoreFileUploaderDelegate.registerHandler(CoreSharedFilesUploadHandler.instance); + CoreSettingsDelegate.registerHandler(CoreSharedFilesSettingsHandler.instance); + + CoreSharedFilesHelper.initialize(); + await CoreSharedFiles.initializeDatabase(); + }, + }, + ], +}) +export class CoreSharedFilesModule {} diff --git a/src/core/singletons/events.ts b/src/core/singletons/events.ts index 14a44e3ff..8b433b74e 100644 --- a/src/core/singletons/events.ts +++ b/src/core/singletons/events.ts @@ -51,6 +51,8 @@ export interface CoreEventsData { [CoreEvents.LOGIN_SITE_CHECKED]: CoreEventLoginSiteCheckedData; [CoreEvents.SEND_ON_ENTER_CHANGED]: CoreEventSendOnEnterChangedData; [CoreEvents.COMPONENT_FILE_ACTION]: CoreFilepoolComponentFileEventData; + [CoreEvents.FILE_SHARED]: CoreEventFileSharedData; + [CoreEvents.APP_LAUNCHED_URL]: CoreEventAppLaunchedData; }; /* @@ -359,3 +361,18 @@ export type CoreEventLoginSiteCheckedData = { export type CoreEventSendOnEnterChangedData = { sendOnEnter: boolean; }; + +/** + * Data passed to FILE_SHARED event. + */ +export type CoreEventFileSharedData = { + name: string; + siteId: string; +}; + +/** + * Data passed to APP_LAUNCHED_URL event. + */ + export type CoreEventAppLaunchedData = { + url: string; +};