diff --git a/tdrive/connectors/onlyoffice-connector/readme.md b/tdrive/connectors/onlyoffice-connector/readme.md index 6811f33d7..cad453bd9 100644 --- a/tdrive/connectors/onlyoffice-connector/readme.md +++ b/tdrive/connectors/onlyoffice-connector/readme.md @@ -18,6 +18,7 @@ Documentation of the [API used with OnlyOffice is here](https://api.onlyoffice.c - The inline configuration provides a read and a write URL to the DocsAPI editor, these point to the routes of `OnlyOfficeController`. - When all the clients have disconnected from the editing session on the OnlyOffice document editing service, the OnlyOffice server will call the `callbackUrl` of this connector. - The connector then downloads the new file from Only Office, and creates a new version in Twake Drive. +- Periodically gets a list of forgotten files and updates the backend with them ## Configuration example diff --git a/tdrive/connectors/onlyoffice-connector/src/app.ts b/tdrive/connectors/onlyoffice-connector/src/app.ts index 65258ba9a..c784dede7 100644 --- a/tdrive/connectors/onlyoffice-connector/src/app.ts +++ b/tdrive/connectors/onlyoffice-connector/src/app.ts @@ -45,8 +45,13 @@ class App { }); this.app.get('/health', (_req, res) => { - Promise.all([onlyofficeService.getLatestVersion(), apiService.hasToken()]).then( - ([version, twakeDriveToken]) => res.status(version && twakeDriveToken ? 200 : 500).send({ version, twakeDriveToken }), + Promise.all([onlyofficeService.getLatestLicence(), apiService.hasToken()]).then( + ([onlyOfficeLicense, twakeDriveToken]) => + res.status(onlyOfficeLicense && twakeDriveToken ? 200 : 500).send({ + uptime: process.uptime(), + onlyOfficeLicense, + twakeDriveToken, + }), err => res.status(500).send(err), ); }); diff --git a/tdrive/connectors/onlyoffice-connector/src/config/index.ts b/tdrive/connectors/onlyoffice-connector/src/config/index.ts index ca0a3f5e1..e25db1b8e 100644 --- a/tdrive/connectors/onlyoffice-connector/src/config/index.ts +++ b/tdrive/connectors/onlyoffice-connector/src/config/index.ts @@ -12,3 +12,7 @@ export const { SERVER_PREFIX, SERVER_ORIGIN, } = process.env; + +export const twakeDriveTokenRefrehPeriodMS = 10 * 60 * 1000; +export const onlyOfficeForgottenFilesCheckPeriodMS = 10 * 60 * 1000; +export const onlyOfficeConnectivityCheckPeriodMS = 10 * 60 * 1000; diff --git a/tdrive/connectors/onlyoffice-connector/src/services/api.service.ts b/tdrive/connectors/onlyoffice-connector/src/services/api.service.ts index 9aff4fde9..a509cf5cb 100644 --- a/tdrive/connectors/onlyoffice-connector/src/services/api.service.ts +++ b/tdrive/connectors/onlyoffice-connector/src/services/api.service.ts @@ -5,28 +5,49 @@ import { IApiServiceApplicationTokenResponse, } from '@/interfaces/api.interface'; import axios, { Axios, AxiosRequestConfig, AxiosResponse } from 'axios'; -import { CREDENTIALS_ENDPOINT, CREDENTIALS_ID, CREDENTIALS_SECRET } from '@config'; +import { + CREDENTIALS_ENDPOINT, + CREDENTIALS_ID, + CREDENTIALS_SECRET, + twakeDriveTokenRefrehPeriodMS, + onlyOfficeForgottenFilesCheckPeriodMS, +} from '@config'; import logger from '../lib/logger'; import * as Utils from '@/utils'; import { PolledThingieValue } from '@/lib/polled-thingie-value'; +import onlyofficeService from './onlyoffice.service'; /** * Client for the Twake Drive backend API on behalf of the plugin (or provided token in parameters). * Periodically updates authorization and adds to requests. */ class ApiService implements IApiService { - private readonly poller: PolledThingieValue; + private readonly tokenPoller: PolledThingieValue; + private readonly forgottenFilesPoller: PolledThingieValue; constructor() { - this.poller = new PolledThingieValue('Refresh Twake Drive token', async () => this.refreshToken(), 1000 * 60); //TODO: should be Every 10 minutes + this.tokenPoller = new PolledThingieValue('Refresh Twake Drive token', async () => await this.refreshToken(), twakeDriveTokenRefrehPeriodMS); + this.forgottenFilesPoller = new PolledThingieValue( + 'Process forgotten files in OO', + async () => await this.processForgottenFiles(), + onlyOfficeForgottenFilesCheckPeriodMS, + ); } public async hasToken() { - return (await this.poller.latestValueWithTry()) !== undefined; + return (await this.tokenPoller.latestValueWithTry()) !== undefined; } private requireAxios() { - return this.poller.requireLatestValueWithTry('Token Kind 538 not ready'); + return this.tokenPoller.requireLatestValueWithTry('No Twake Drive app token.'); + } + + private async processForgottenFiles() { + if (!this.tokenPoller.hasValue()) return -1; + return await onlyofficeService.processForgotten(async (/* key, url */) => { + //TODO: when endpoint decided, call here. See if accept HTTP 202 for ex. to avoid deleting. + return false; + }); } public get = async (params: IApiServiceRequestParams): Promise => { diff --git a/tdrive/connectors/onlyoffice-connector/src/services/onlyoffice.service.ts b/tdrive/connectors/onlyoffice-connector/src/services/onlyoffice.service.ts index 39e52d5a9..d0f8e07d5 100644 --- a/tdrive/connectors/onlyoffice-connector/src/services/onlyoffice.service.ts +++ b/tdrive/connectors/onlyoffice-connector/src/services/onlyoffice.service.ts @@ -1,5 +1,5 @@ import axios from 'axios'; -import { ONLY_OFFICE_SERVER } from '@config'; +import { ONLY_OFFICE_SERVER, onlyOfficeConnectivityCheckPeriodMS } from '@config'; import { PolledThingieValue } from '@/lib/polled-thingie-value'; import logger from '@/lib/logger'; import * as Utils from '@/utils'; @@ -161,6 +161,20 @@ namespace CommandService { } } } + + export namespace License { + export interface Response extends SuccessResponse { + // @see https://api.onlyoffice.com/editors/command/license + license: object; + server: object; + quota: object; + } + export class Request extends BaseRequest { + constructor() { + super('license'); + } + } + } } /** @@ -168,18 +182,52 @@ namespace CommandService { * @see https://api.onlyoffice.com/editors/command/ */ class OnlyOfficeService { - private readonly poller: PolledThingieValue; + private readonly poller: PolledThingieValue; + constructor() { - this.poller = new PolledThingieValue('Connect to Only Office', () => this.getVersion(), 10 * 1000 * 60); + this.poller = new PolledThingieValue( + 'Connect to Only Office', + async () => { + logger.info('Only Office license status'); + return await this.getLicense(); + }, + onlyOfficeConnectivityCheckPeriodMS, + ); } - /** Get the latest Only Office version from polling. If the return is `undefined` + /** Get the latest Only Office licence status from polling. If the return is `undefined` * it probably means there is a connection issue contacting the OnlyOffice server * from the connector. */ - public getLatestVersion() { + public getLatestLicence() { return this.poller.latest(); } + /** + * Iterate over all the forgotten files as returned by OnlyOffice, call the processor for each.. + * @param processor Handler to process the forgotten file (available at `url`). If `true` is returned, + * the file is deleted from the forgotten file list in OnlyOffice. If false is returned, the + * same forgotten file will reappear in a future batch + * @returns The number of files processed and deleted + */ + public async processForgotten(processor: (key: string, url: string) => Promise): Promise { + const forgottenFiles = await this.getForgottenList(); + if (forgottenFiles.length === 0) return 0; + Utils.fisherYattesShuffleInPlace(forgottenFiles); + logger.info(`Forgotten files found: ${forgottenFiles.length}`); + let deleted = 0; + for (const forgottenFileKey of forgottenFiles) { + const forgottenFileURL = await this.getForgotten(forgottenFileKey); + logger.info(`Forgotten file about to process: ${JSON.stringify(forgottenFileKey)}`, { url: forgottenFileURL }); + const shouldDelete = await processor(forgottenFileKey, forgottenFileURL); + if (shouldDelete) { + logger.info(`Forgotten file about to be deleted: ${JSON.stringify(forgottenFileKey)}`); + await this.deleteForgotten(forgottenFileKey); + deleted++; + } + } + return deleted; + } + // Note that `async` is important in the functions below. While they avoid the overhead // of `await`, the `async` is still required to catch the throw in `.post()` @@ -187,6 +235,11 @@ class OnlyOfficeService { async getVersion(): Promise { return new CommandService.Version.Request().post().then(response => response.version); } + /** Return the version string of OnlyOffice */ + async getLicense(): Promise { + //TODO: When typing the response more fully, don't return the response object itself as here + return new CommandService.License.Request().post(); + } /** Force a save in the editing session key provided. `userdata` will be forwarded to the callback */ async forceSave(key: string, userdata = ''): Promise { return new CommandService.ForceSave.Request(key, userdata).post().then(response => response.key); diff --git a/tdrive/connectors/onlyoffice-connector/src/utils.ts b/tdrive/connectors/onlyoffice-connector/src/utils.ts index 094781722..1fc1d55b3 100644 --- a/tdrive/connectors/onlyoffice-connector/src/utils.ts +++ b/tdrive/connectors/onlyoffice-connector/src/utils.ts @@ -44,3 +44,14 @@ export function splitFilename(filename: string): [string, string] { const extension = parts.pop(); return [parts.join('.'), extension]; } + +/** Shuffle an array in place, returns its parameter for convenience */ +export function fisherYattesShuffleInPlace(list: T[]): T[] { + let index = list.length; + while (index) { + const randomIndex = Math.floor(Math.random() * index); + index--; + [list[index], list[randomIndex]] = [list[randomIndex], list[index]]; + } + return list; +}