✨ 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`.
|
- 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.
|
- 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.
|
- 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
|
## Configuration example
|
||||||
|
|
||||||
|
|||||||
@@ -45,8 +45,13 @@ class App {
|
|||||||
});
|
});
|
||||||
|
|
||||||
this.app.get('/health', (_req, res) => {
|
this.app.get('/health', (_req, res) => {
|
||||||
Promise.all([onlyofficeService.getLatestVersion(), apiService.hasToken()]).then(
|
Promise.all([onlyofficeService.getLatestLicence(), apiService.hasToken()]).then(
|
||||||
([version, twakeDriveToken]) => res.status(version && twakeDriveToken ? 200 : 500).send({ version, twakeDriveToken }),
|
([onlyOfficeLicense, twakeDriveToken]) =>
|
||||||
|
res.status(onlyOfficeLicense && twakeDriveToken ? 200 : 500).send({
|
||||||
|
uptime: process.uptime(),
|
||||||
|
onlyOfficeLicense,
|
||||||
|
twakeDriveToken,
|
||||||
|
}),
|
||||||
err => res.status(500).send(err),
|
err => res.status(500).send(err),
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -12,3 +12,7 @@ export const {
|
|||||||
SERVER_PREFIX,
|
SERVER_PREFIX,
|
||||||
SERVER_ORIGIN,
|
SERVER_ORIGIN,
|
||||||
} = process.env;
|
} = 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,
|
IApiServiceApplicationTokenResponse,
|
||||||
} from '@/interfaces/api.interface';
|
} from '@/interfaces/api.interface';
|
||||||
import axios, { Axios, AxiosRequestConfig, AxiosResponse } from 'axios';
|
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 logger from '../lib/logger';
|
||||||
import * as Utils from '@/utils';
|
import * as Utils from '@/utils';
|
||||||
import { PolledThingieValue } from '@/lib/polled-thingie-value';
|
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).
|
* Client for the Twake Drive backend API on behalf of the plugin (or provided token in parameters).
|
||||||
* Periodically updates authorization and adds to requests.
|
* Periodically updates authorization and adds to requests.
|
||||||
*/
|
*/
|
||||||
class ApiService implements IApiService {
|
class ApiService implements IApiService {
|
||||||
private readonly poller: PolledThingieValue<Axios>;
|
private readonly tokenPoller: PolledThingieValue<Axios>;
|
||||||
|
private readonly forgottenFilesPoller: PolledThingieValue<number>;
|
||||||
|
|
||||||
constructor() {
|
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() {
|
public async hasToken() {
|
||||||
return (await this.poller.latestValueWithTry()) !== undefined;
|
return (await this.tokenPoller.latestValueWithTry()) !== undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
private requireAxios() {
|
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> => {
|
public get = async <T>(params: IApiServiceRequestParams<T>): Promise<T> => {
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import axios from 'axios';
|
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 { PolledThingieValue } from '@/lib/polled-thingie-value';
|
||||||
import logger from '@/lib/logger';
|
import logger from '@/lib/logger';
|
||||||
import * as Utils from '@/utils';
|
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/
|
* @see https://api.onlyoffice.com/editors/command/
|
||||||
*/
|
*/
|
||||||
class OnlyOfficeService {
|
class OnlyOfficeService {
|
||||||
private readonly poller: PolledThingieValue<string>;
|
private readonly poller: PolledThingieValue<CommandService.License.Response>;
|
||||||
|
|
||||||
constructor() {
|
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
|
* it probably means there is a connection issue contacting the OnlyOffice server
|
||||||
* from the connector.
|
* from the connector.
|
||||||
*/
|
*/
|
||||||
public getLatestVersion() {
|
public getLatestLicence() {
|
||||||
return this.poller.latest();
|
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
|
// 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()`
|
// of `await`, the `async` is still required to catch the throw in `.post()`
|
||||||
|
|
||||||
@@ -187,6 +235,11 @@ class OnlyOfficeService {
|
|||||||
async getVersion(): Promise<string> {
|
async getVersion(): Promise<string> {
|
||||||
return new CommandService.Version.Request().post().then(response => response.version);
|
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 */
|
/** Force a save in the editing session key provided. `userdata` will be forwarded to the callback */
|
||||||
async forceSave(key: string, userdata = ''): Promise<string> {
|
async forceSave(key: string, userdata = ''): Promise<string> {
|
||||||
return new CommandService.ForceSave.Request(key, userdata).post().then(response => response.key);
|
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();
|
const extension = parts.pop();
|
||||||
return [parts.join('.'), extension];
|
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