MOBILE-3630 sharefiles: Implement services
parent
f1c6e5dd28
commit
92f5742351
|
@ -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);
|
||||
});
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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 {}
|
||||
|
|
|
@ -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."
|
||||
}
|
|
@ -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;
|
||||
};
|
|
@ -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<boolean> {
|
||||
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);
|
|
@ -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<boolean> {
|
||||
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<CoreFileUploaderHandlerResult> => CoreSharedFilesHelper.pickSharedFile(mimetypes),
|
||||
};
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
export const CoreSharedFilesUploadHandler = makeSingleton(CoreSharedFilesUploadHandlerService);
|
|
@ -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<string> {
|
||||
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<CoreFileUploaderHandlerResult> {
|
||||
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<void> {
|
||||
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<void> {
|
||||
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<void> {
|
||||
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);
|
|
@ -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<SQLiteDB>;
|
||||
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<void> {
|
||||
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<FileEntry | undefined> {
|
||||
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 = <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<void> {
|
||||
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 <string> 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<CoreSharedFilesDBRecord> {
|
||||
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<void> {
|
||||
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<FileEntry | undefined> {
|
||||
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<void> {
|
||||
const db = await this.appDB;
|
||||
|
||||
await db.deleteRecords(SHARED_FILES_TABLE_NAME, { id: fileId });
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
export const CoreSharedFiles = makeSingleton(CoreSharedFilesProvider);
|
|
@ -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<unknown>[] = [
|
||||
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 {}
|
|
@ -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;
|
||||
};
|
||||
|
|
Loading…
Reference in New Issue