✨ ooconnector: poll forgotten files, wip: extract code from oocallback to call here (#525)
This commit is contained in:
@@ -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
|
||||
|
||||
|
||||
@@ -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),
|
||||
);
|
||||
});
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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<Axios>;
|
||||
private readonly tokenPoller: PolledThingieValue<Axios>;
|
||||
private readonly forgottenFilesPoller: PolledThingieValue<number>;
|
||||
|
||||
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 <T>(params: IApiServiceRequestParams<T>): Promise<T> => {
|
||||
|
||||
@@ -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<Response> {
|
||||
constructor() {
|
||||
super('license');
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -168,18 +182,52 @@ namespace CommandService {
|
||||
* @see https://api.onlyoffice.com/editors/command/
|
||||
*/
|
||||
class OnlyOfficeService {
|
||||
private readonly poller: PolledThingieValue<string>;
|
||||
private readonly poller: PolledThingieValue<CommandService.License.Response>;
|
||||
|
||||
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<boolean>): Promise<number> {
|
||||
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<string> {
|
||||
return new CommandService.Version.Request().post().then(response => response.version);
|
||||
}
|
||||
/** Return the version string of OnlyOffice */
|
||||
async getLicense(): Promise<CommandService.License.Response> {
|
||||
//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<string> {
|
||||
return new CommandService.ForceSave.Request(key, userdata).post().then(response => response.key);
|
||||
|
||||
@@ -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<T>(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;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user