diff --git a/src/core/sharedfiles/lang/en.json b/src/core/sharedfiles/lang/en.json new file mode 100644 index 000000000..c7ba93a40 --- /dev/null +++ b/src/core/sharedfiles/lang/en.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/sharedfiles/providers/sharedfiles.ts b/src/core/sharedfiles/providers/sharedfiles.ts new file mode 100644 index 000000000..14f838f40 --- /dev/null +++ b/src/core/sharedfiles/providers/sharedfiles.ts @@ -0,0 +1,243 @@ +// (C) Copyright 2015 Martin Dougiamas +// +// 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 { CoreAppProvider } from '../../../providers/app'; +import { CoreEventsProvider } from '../../../providers/events'; +import { CoreFileProvider } from '../../../providers/file'; +import { CoreLoggerProvider } from '../../../providers/logger'; +import { CoreSitesProvider } from '../../../providers/sites'; +import { CoreMimetypeUtilsProvider } from '../../../providers/utils/mimetype'; +import { CoreTextUtilsProvider } from '../../../providers/utils/text'; +import { Md5 } from 'ts-md5/dist/md5'; +import { SQLiteDB } from '../../../classes/sqlitedb'; + +/** + * Service to share files with the app. + */ +@Injectable() +export class CoreSharedFilesProvider { + public static SHARED_FILES_FOLDER = 'sharedfiles'; + + // Variables for the database. + protected SHARED_FILES_TABLE = 'wscache'; + protected tableSchema = { + name: this.SHARED_FILES_TABLE, + columns: [ + { + name: 'id', + type: 'TEXT', + primaryKey: true + } + ] + } + + protected logger; + protected appDB: SQLiteDB; + + constructor(logger: CoreLoggerProvider, private fileProvider: CoreFileProvider, appProvider: CoreAppProvider, + private textUtils: CoreTextUtilsProvider, private mimeUtils: CoreMimetypeUtilsProvider, + private sitesProvider: CoreSitesProvider, private eventsProvider: CoreEventsProvider) { + this.logger = logger.getInstance('CoreSharedFilesProvider'); + + this.appDB = appProvider.getDB(); + this.appDB.createTableFromSchema(this.tableSchema); + } + + /** + * 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} Promise resolved with a new file to be treated. If no new files found, promise is rejected. + */ + checkIOSNewFiles() : Promise { + this.logger.debug('Search for new files on iOS'); + return this.fileProvider.getDirectoryContents('Inbox').then((entries) => { + if (entries.length > 0) { + let promises = [], + fileToReturn; + + entries.forEach((entry) => { + const fileId = this.getFileId(entry); + + // Check if file was already treated. + promises.push(this.isFileTreated(fileId).then(() => { + // File already treated, delete it. Don't return delete promise, we'll ignore errors. + this.deleteInboxFile(entry); + }).catch(() => { + // File not treated before. + this.logger.debug('Found new file ' + entry.name + ' shared with the app.'); + if (!fileToReturn) { + fileToReturn = entry; + } + })); + }); + + return Promise.all(promises).then(() => { + let fileId; + + if (fileToReturn) { + // Mark it as "treated". + fileId = this.getFileId(fileToReturn); + return this.markAsTreated(fileId).then(() => { + this.logger.debug('File marked as "treated": ' + fileToReturn.name); + return fileToReturn; + }); + } else { + return Promise.reject(null); + } + }); + } else { + return Promise.reject(null); + } + }); + } + + /** + * Deletes a file in the Inbox folder (shared with the app). + * + * @param {any} entry FileEntry. + * @return {Promise} Promise resolved when done, rejected otherwise. + */ + deleteInboxFile(entry: any) : Promise { + this.logger.debug('Delete inbox file: ' + entry.name); + + return this.fileProvider.removeFileByFileEntry(entry).catch(() => { + // Ignore errors. + }).then(() => { + return this.unmarkAsTreated(this.getFileId(entry)).then(() => { + this.logger.debug('"Treated" mark removed from file: ' + entry.name); + }).catch((error) => { + this.logger.debug('Error deleting "treated" mark from file: ' + entry.name, error); + return Promise.reject(error); + }); + }); + } + + /** + * Get the ID of a file for managing "treated" files. + * + * @param {any} entry FileEntry. + * @return {string} File ID. + */ + protected getFileId(entry: any) : string { + return Md5.hashAsciiStr(entry.name); + } + + /** + * Get the shared files stored in a site. + * + * @param {string} [siteId] Site ID. If not defined, current site. + * @param {string} [path] Path to search inside the site shared folder. + * @param {string[]} [mimetypes] List of supported mimetypes. If undefined, all mimetypes supported. + * @return {Promise} Promise resolved with the files. + */ + getSiteSharedFiles(siteId?: string, path?: string, mimetypes?: string[]) : Promise { + let pathToGet = this.getSiteSharedFilesDirPath(siteId); + if (path) { + pathToGet = this.textUtils.concatenatePaths(pathToGet, path); + } + + return this.fileProvider.getDirectoryContents(pathToGet).then((files) => { + if (mimetypes) { + // Only show files with the right mimetype and the ones we cannot determine the mimetype. + files = files.filter((file) => { + const extension = this.mimeUtils.getFileExtension(file.name), + mimetype = this.mimeUtils.getMimeType(extension); + + return !mimetype || mimetypes.indexOf(mimetype) > -1; + }); + } + + return files; + }).catch(() => { + // Directory not found, return empty list. + return []; + }); + } + + /** + * Get the path to a site's shared files folder. + * + * @param {string} [siteId] Site ID. If not defined, current site. + * @return {string} Path. + */ + getSiteSharedFilesDirPath(siteId?: string) : string { + siteId = siteId || this.sitesProvider.getCurrentSiteId(); + return this.fileProvider.getSiteFolder(siteId) + '/' + CoreSharedFilesProvider.SHARED_FILES_FOLDER; + } + + /** + * Check if a file has been treated already. + * + * @param {string} fileId File ID. + * @return {Promise} Resolved if treated, rejected otherwise. + */ + protected isFileTreated(fileId: string) : Promise { + return this.appDB.getRecord(this.SHARED_FILES_TABLE, {id: fileId}); + } + + /** + * Mark a file as treated. + * + * @param {string} fileId File ID. + * @return {Promise} Promise resolved when marked. + */ + protected markAsTreated(fileId: string) : Promise { + // Check if it's already marked. + return this.isFileTreated(fileId).catch(() => { + // Doesn't exist, insert it. + return this.appDB.insertRecord(this.SHARED_FILES_TABLE, {id: fileId}); + }); + } + + /** + * Store a file in a site's shared folder. + * + * @param {any} entry File entry. + * @param {string} [newName] Name of the new file. If not defined, use original file's name. + * @param {string} [siteId] Site ID. If not defined, current site. + * @return {Promise}Promise resolved when done. + */ + storeFileInSite(entry: any, newName?: string, siteId?: string) : Promise { + siteId = siteId || this.sitesProvider.getCurrentSiteId(); + + if (!entry || !siteId) { + return Promise.reject(null); + } + + newName = newName || entry.name; + + const sharedFilesFolder = this.getSiteSharedFilesDirPath(siteId), + newPath = this.textUtils.concatenatePaths(sharedFilesFolder, newName); + + // Create dir if it doesn't exist already. + return this.fileProvider.createDir(sharedFilesFolder).then(() => { + return this.fileProvider.moveFile(entry.fullPath, newPath).then((newFile) => { + this.eventsProvider.trigger(CoreEventsProvider.FILE_SHARED, {siteid: siteId, name: newName}); + return newFile; + }); + }); + } + + /** + * Unmark a file as treated. + * + * @param {string} fileId File ID. + * @return {Promise} Resolved when unmarked. + */ + protected unmarkAsTreated(fileId: string) : Promise { + return this.appDB.deleteRecords(this.SHARED_FILES_TABLE, {id: fileId}); + } +} diff --git a/src/core/sharedfiles/sharedfiles.module.ts b/src/core/sharedfiles/sharedfiles.module.ts new file mode 100644 index 000000000..eee25251a --- /dev/null +++ b/src/core/sharedfiles/sharedfiles.module.ts @@ -0,0 +1,27 @@ +// (C) Copyright 2015 Martin Dougiamas +// +// 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 { NgModule } from '@angular/core'; +import { CoreSharedFilesProvider } from './providers/sharedfiles'; + +@NgModule({ + declarations: [ + ], + imports: [ + ], + providers: [ + CoreSharedFilesProvider + ] +}) +export class CoreSharedFilesModule {} diff --git a/src/providers/events.ts b/src/providers/events.ts index 27aeb835a..e9066b00f 100644 --- a/src/providers/events.ts +++ b/src/providers/events.ts @@ -46,6 +46,7 @@ export class CoreEventsProvider { public static IAB_LOAD_START = 'inappbrowser_load_start'; public static IAB_EXIT = 'inappbrowser_exit'; public static APP_LAUNCHED_URL = 'app_launched_url'; // App opened with a certain URL (custom URL scheme). + public static FILE_SHARED = 'file_shared'; logger; observables: {[s: string] : Subject} = {};