Skip to content

Commit

Permalink
♻️🎨 connector: light cleanup, load (#525)
Browse files Browse the repository at this point in the history
renaming logger, adding health endpoint, working with old API,
removing aggressive force save stuff, missing forgotten files etc,
documentation
  • Loading branch information
ericlinagora committed Jul 11, 2024
1 parent 0d7a319 commit 14fdbb2
Show file tree
Hide file tree
Showing 17 changed files with 303 additions and 175 deletions.
1 change: 1 addition & 0 deletions tdrive/connectors/onlyoffice-connector/.eslintrc
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@
"@typescript-eslint/explicit-module-boundary-types": 0,
"@typescript-eslint/no-explicit-any": "off",
"@typescript-eslint/ban-types": "off",
"@typescript-eslint/no-namespace": "off",
"@typescript-eslint/no-var-requires": "off"
}
}
17 changes: 13 additions & 4 deletions tdrive/connectors/onlyoffice-connector/src/app.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,10 @@ import { renderFile } from 'eta';
import path from 'path';
import errorMiddleware from './middlewares/error.middleware';
import { SERVER_PORT, SERVER_PREFIX } from '@config';
import loggerService from './services/logger.service';
import logger from './lib/logger';
import cookieParser from 'cookie-parser';
import apiService from './services/api.service';
import onlyofficeService from './services/onlyoffice.service';

class App {
public app: express.Application;
Expand All @@ -25,23 +27,30 @@ class App {
}

public listen = () => {
this.app.listen(SERVER_PORT, () => {
loggerService.info(`🚀 App listening on port ${SERVER_PORT}`);
this.app.listen(parseInt(SERVER_PORT, 10), '0.0.0.0', () => {
logger.info(`🚀 App listening on port ${SERVER_PORT}`);
});
};

public getServer = () => this.app;

private initRoutes = (routes: Routes[]) => {
this.app.use((req, res, next) => {
console.log('Requested: ', req.method, req.originalUrl);
logger.info(`Received request: ${req.method} ${req.originalUrl} from ${req.header('user-agent')} (${req.ip})`);
next();
});

routes.forEach(route => {
this.app.use(SERVER_PREFIX, route.router);
});

this.app.get('/health', (_req, res) => {
Promise.all([onlyofficeService.getLatestVersion(), apiService.hasToken()]).then(
([version, twakeDriveToken]) => res.status(version && twakeDriveToken ? 200 : 500).send({ version, twakeDriveToken }),
err => res.status(500).send(err),
);
});

this.app.use(
SERVER_PREFIX.replace(/\/$/, '') + '/assets',
(req, res, next) => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import driveService from '@/services/drive.service';
import { DriveFileType } from '@/interfaces/drive.interface';
import fileService from '@/services/file.service';
import { OfficeToken } from '@/interfaces/routes.interface';
import loggerService from '@/services/logger.service';
import logger from '@/lib/logger';
import * as Utils from '@/utils';

interface RequestQuery {
Expand Down Expand Up @@ -96,7 +96,7 @@ class IndexController {
company_id,
preview,
office_token: officeToken,
})
}),
);
} catch (error) {
next(error);
Expand Down Expand Up @@ -134,7 +134,7 @@ class IndexController {
token: inPageToken,
});
} catch (error) {
loggerService.error(error);
logger.error(error);
next(error);
}
};
Expand Down
Original file line number Diff line number Diff line change
@@ -1,10 +1,9 @@
import { CREDENTIALS_SECRET } from '@/config';
import { OfficeToken } from '@/interfaces/routes.interface';
import apiService from '@/services/api.service';
import driveService from '@/services/drive.service';
import fileService from '@/services/file.service';
import loggerService from '@/services/logger.service';
import onlyofficeService, * as OnlyOffice from '@/services/onlyoffice.service';
import logger from '@/lib/logger';
import * as OnlyOffice from '@/services/onlyoffice.service';
import { Request, Response, NextFunction } from 'express';
import jwt from 'jsonwebtoken';

Expand All @@ -14,14 +13,6 @@ interface RequestQuery {
token: string;
}

interface SaveRequestBody {
filetype: string;
key: string;
status: number;
url: string;
users: string[];
}

/** These expose a OnlyOffice document storage service methods, called by the OnlyOffice document editing service
* to load and save files
*/
Expand Down Expand Up @@ -66,78 +57,72 @@ class OnlyOfficeController {
};

/**
* Receive a file from OnlyOffice document editing service and save it into Twake Drive backend
* Receive a file from OnlyOffice document editing service and save it into Twake Drive backend.
*
* This is the endpoint of the callback url provided to the editor in the browser.
*
* Parameters are standard Express middleware.
* @see https://api.onlyoffice.com/editors/save
* @see https://api.onlyoffice.com/editors/callback
*/
public save = async (req: Request<{}, {}, SaveRequestBody, RequestQuery>, res: Response, next: NextFunction): Promise<void> => {
public ooCallback = async (
req: Request<{}, {}, OnlyOffice.Callback.Parameters, RequestQuery>,
res: Response,
next: NextFunction,
): Promise<void> => {
const respondToOO = (error = 0) => void res.send({ error });
try {
const { url, key } = req.body;
const { token } = req.query;
loggerService.info('Save request', { key, url, token });
logger.info('Save request', { key, url, token });

const officeTokenPayload = jwt.verify(token, CREDENTIALS_SECRET) as OfficeToken;
const { preview, company_id, file_id, user_id, drive_file_id, in_page_token } = officeTokenPayload;
const { preview, company_id, file_id, /* user_id, */ drive_file_id, in_page_token } = officeTokenPayload;

// check token is an in_page_token and allow save
if (!in_page_token) throw new Error('Invalid token, must be a in_page_token');
if (preview) throw new Error('Invalid token, must not be a preview token for save operation');

if (url) {
loggerService.info('URL present, saving file');
// If token indicate a drive_file_id then check if we want to create a new version or not
if (drive_file_id) {
loggerService.info('Drive file id present, checking if we need to create a new version');
//Get the drive file
const driveFile = await driveService.get({
switch (req.body.status) {
case OnlyOffice.Callback.Status.BEING_EDITED:
case OnlyOffice.Callback.Status.BEING_EDITED_BUT_IS_SAVED:
// No-op
break;

case OnlyOffice.Callback.Status.READY_FOR_SAVING:
const newVersionFile = await fileService.save({
company_id,
file_id,
url,
create_new: true,
});

const version = await driveService.createVersion({
company_id,
drive_file_id,
file_id: newVersionFile?.resource?.id,
});
logger.info('New version created', version);
return respondToOO();

// const createNewVersion = !!driveFile; //Always create a new version because needed by OnlyOffice // driveFile.item.last_version_cache.date_added < Date.now() - 1000 * 60 * 60 * 3;
const createNewVersion = true;
if (createNewVersion) {
const newVersionFile = await fileService.save({
company_id,
file_id,
url,
create_new: true,
});

await driveService.createVersion({
company_id,
drive_file_id,
file_id: newVersionFile?.resource?.id,
});
loggerService.info('New version created');

res.send({
error: 0,
});

return;
}
}
loggerService.info('Saving file');
case OnlyOffice.Callback.Status.CLOSED_WITHOUT_CHANGES:
// Save end of transaction
break;

await fileService.save({
company_id,
file_id,
url,
user_id: user_id,
});
} else {
loggerService.error('URL not present, force saving file');
await onlyofficeService.forceSave(key).catch(error => {
loggerService.warn("Expected error while force saving without changes (ignored):", error);
});
}
case OnlyOffice.Callback.Status.ERROR_SAVING:
// Save end of transaction
logger.info(`Error saving file ${req.body.url} (key: ${req.body.key})`);
break;

res.send({
error: 0,
});
case OnlyOffice.Callback.Status.ERROR_FORCE_SAVING:
// TODO: notify user ?
logger.info(`Error force saving (reason: ${req.body.forcesavetype}) file ${req.body.url} (key: ${req.body.key})`);
return void res.send({ error: 0 });

default:
throw new Error(`Unexpected OO Callback status field: ${req.body.status}`);
}
return respondToOO();
} catch (error) {
next(error);
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
import logger from './logger';

const now = () => new Date().getTime();
type NotUndefined<T> = T extends undefined ? never : T;

/**
* Hold a value that is periodically obtained from a fallible process, and
* track update times and errors.
*/
export class PolledThingieValue<ValueType> {
private lastOkTimeMs: number | undefined;
private lastKoTimeMs: number | undefined;
protected lastValue: ValueType | undefined;
protected lastKoError: any;
protected pendingTry: Promise<ValueType | undefined> | undefined;

constructor(
private readonly logPrefix: string,
private readonly getTheThingieValue: () => Promise<NotUndefined<ValueType>>,
private readonly intervalMs: number,
) {
this.run();
setInterval(() => this.run(), this.intervalMs);
}

protected setResult(value: undefined, error?: NotUndefined<ValueType>, ts?: number);
protected setResult(value: NotUndefined<ValueType>, error?: undefined, ts?: number);
protected setResult(value: ValueType | undefined, error?: any, ts = now()) {
if (value && error) throw new Error(`Unexpected value (${JSON.stringify(value)}) and error (${JSON.stringify(error)})`);
if (error) this.lastKoTimeMs = ts;
else this.lastOkTimeMs = ts;
this.lastValue = value;
this.lastKoError = error;
if (error) logger.error(this.logPrefix + ' error:', error.stack);
}

private async run() {
if (this.pendingTry) return this.pendingTry;
return (this.pendingTry = new Promise((resolve, reject) => {
this.getTheThingieValue().then(
value => {
this.setResult(value === undefined ? null : value);
this.pendingTry = undefined;
resolve(value);
},
error => {
this.setResult(undefined, error.stack);
this.pendingTry = undefined;
reject(error);
},
);
}));
}

public lastFailed() {
return !!this.lastKoTimeMs;
}

public hasValue() {
return !!this.lastOkTimeMs;
}

/** Get the latest value and age in seconds if it was successful, or `undefined` */
public latest(): { value: ValueType; ageS: number } | undefined {
if (!this.hasValue()) return undefined;
return {
value: this.lastValue,
ageS: Math.floor((now() - this.lastOkTimeMs) / 1000),
};
}

/** Get the latest value if it was successful, or `undefined` */
public latestValue(): ValueType | undefined {
if (!this.hasValue()) return undefined;
return this.lastValue;
}

/**
* Get a promise to the latest value. If there isn't one, try to get one first.
* New requests when one is already pending will not cause a separate run,
* but get the previous promise back.
**/
public async latestValueWithTry(): Promise<ValueType | undefined> {
if (this.hasValue()) return this.lastValue;
if (this.pendingTry) return this.pendingTry;
return this.run();
}

/**
* Use {@see latestValueWithTry} and if the result is still `undefined`, reject
* the promise with the provided `errorMessage`.
**/
public async requireLatestValueWithTry(errorMessage: string): Promise<ValueType> {
const result = await this.latestValueWithTry();
if (result === undefined) throw new Error(errorMessage);
return result;
}
}
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { CREDENTIALS_SECRET } from '@/config';
import loggerService from '@/services/logger.service';
import logger from '@/lib/logger';
import userService from '@/services/user.service';
import { NextFunction, Request, Response } from 'express';
import jwt from 'jsonwebtoken';
Expand All @@ -25,7 +25,7 @@ export default async (req: Request<{}, {}, {}, RequestQuery>, res: Response, nex
return;
}
} catch (error) {
loggerService.error('invalid token', error);
logger.error('invalid token', error.stack);
res.clearCookie('token');
}
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
import loggerService from '@/services/logger.service';
import logger from '@/lib/logger';
import { NextFunction, Request, Response } from 'express';

export default (error: Error & { status?: number }, req: Request, res: Response, next: NextFunction): void => {
try {
const status: number = error.status || 500;
const message: string = error.message || 'something went wrong';

loggerService.error(`[${req.method}] ${req.path} >> StatusCode:: ${status}, Message:: ${message}`);
logger.error(`[${req.method}] ${req.path} >> StatusCode:: ${status}, Message:: ${message}`, error.stack);

res.status(status).json({ message });
} catch (error) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,10 @@ import authMiddleware from '@/middlewares/auth.middleware';
import requirementsMiddleware from '@/middlewares/requirements.middleware';
import { Router } from 'express';

/**
* When the user previews or edits a file in Twake Drive, their browser is sent to these routes
* which return a webpage that instantiates the client side JS Only Office component.
*/
class IndexRoute implements Routes {
public path = '/';
public router = Router();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ class OnlyOfficeRoute implements Routes {

private initRoutes = () => {
this.router.get(`${this.path}:mode/read`, requirementsMiddleware, this.onlyOfficeController.read);
this.router.post(`${this.path}:mode/save`, requirementsMiddleware, this.onlyOfficeController.save);
this.router.post(`${this.path}:mode/save`, requirementsMiddleware, this.onlyOfficeController.ooCallback);
};
}

Expand Down
Loading

0 comments on commit 14fdbb2

Please sign in to comment.