♻️ oo-connector: single request endediting, refactor URLs, bit of logging (#525)

This commit is contained in:
Eric Doughty-Papassideris
2024-09-18 17:47:55 +02:00
parent 6685b7b98d
commit 488d5a5936
4 changed files with 28 additions and 53 deletions
@@ -23,5 +23,4 @@ export interface IDriveService {
beginEditingSession: (company_id: string, drive_file_id: string) => Promise<string>;
addEditingSessionVersion: (editing_session_key: string, url: string) => Promise<void>;
endEditing: (editing_session_key: string, url: string) => Promise<void>;
getByEditingSessionKey: (params: { company_id: string; editing_session_key: string; user_token?: string }) => Promise<DriveFileType['item']>;
}
@@ -83,8 +83,9 @@ class ApiService implements IApiService {
},
});
} catch (error) {
logger.error('Failed to post to Twake Drive: ', { error });
logger.error('Failed to post to Twake Drive: ', { error: error.stack });
this.refreshToken();
throw error;
}
};
@@ -1,6 +1,7 @@
import { DriveFileType, IDriveService } from '@/interfaces/drive.interface';
import apiService from './api.service';
import logger from '../lib/logger';
import * as Utils from '../utils';
import { Stream } from 'stream';
import FormData from 'form-data';
import { INSTANCE_ID } from '@/config';
@@ -8,6 +9,12 @@ import { INSTANCE_ID } from '@/config';
/** Thrown when Twake Drive returns a 404 for a key */
export class UnknownKeyInDriveError extends Error {}
const makeTwakeDriveApiUrl = (paths: string[], params?: Utils.QueryParams) => Utils.joinURL(['/internal/services/documents/v1', ...paths], params);
const makeNonEditingSessionItemUrl = (company_id: string, drive_file_id: string, paths: string[] = [], params?: Utils.QueryParams) =>
makeTwakeDriveApiUrl(['companies', company_id, 'item', drive_file_id, ...paths], params);
const makeEditingSessionItemUrl = (editing_session: string, params?: Utils.QueryParams) =>
makeTwakeDriveApiUrl(['editing_session', editing_session], params);
/**
* Client for Twake Drive's APIs dealing with `DriveItem`s, using {@see apiService}
* to handle authorization
@@ -17,7 +24,7 @@ class DriveService implements IDriveService {
try {
const { company_id, drive_file_id } = params;
const resource = await apiService.get<DriveFileType>({
url: `/internal/services/documents/v1/companies/${company_id}/item/${drive_file_id}`,
url: makeNonEditingSessionItemUrl(company_id, drive_file_id),
token: params.user_token,
});
@@ -37,7 +44,7 @@ class DriveService implements IDriveService {
try {
const { company_id, drive_file_id, file_id } = params;
return await apiService.post<{}, DriveFileType['item']['last_version_cache']>({
url: `/internal/services/documents/v1/companies/${company_id}/item/${drive_file_id}/version`,
url: makeNonEditingSessionItemUrl(company_id, drive_file_id, ['version']),
payload: {
drive_item_id: drive_file_id,
provider: 'internal',
@@ -56,7 +63,7 @@ class DriveService implements IDriveService {
public async beginEditingSession(company_id: string, drive_file_id: string, user_token?: string) {
try {
const resource = await apiService.post<{}, { editingSessionKey: string }>({
url: `/internal/services/documents/v1/companies/${company_id}/item/${drive_file_id}/editing_session`,
url: makeNonEditingSessionItemUrl(company_id, drive_file_id, ['editing_session']),
token: user_token,
payload: {
editorApplicationId: 'tdrive_onlyoffice',
@@ -77,7 +84,7 @@ class DriveService implements IDriveService {
public async cancelEditing(editing_session_key: string) {
try {
await apiService.delete<{}>({
url: `/internal/services/documents/v1/editing_session/${encodeURIComponent(editing_session_key)}`,
url: makeEditingSessionItemUrl(editing_session_key),
});
} catch (error) {
logger.error('Failed to begin editing session: ', error.stack);
@@ -99,21 +106,6 @@ class DriveService implements IDriveService {
if (!url) {
throw Error('no url found');
}
//TODO: It would be better to avoid two requests for this operation
const originalFile = await this.getByEditingSessionKey({ editing_session_key });
if (!originalFile) {
// TODO: Make a single request and don't require the filename at all
// then in POST /editing_session/... if the key is not found, just
// put it in a users or company "lost and found" folder.
// and accept without error. Because really, if backend doesn't know
// the key anymore, there's not much we can do, and we should get OO
// to clean up and stop trying to upload it.
// but for today:
logger.error("Forgotten OO document that Twake Drive doesn't know the key of.", { editing_session_key, url });
throw new UnknownKeyInDriveError(`Unknown key ${JSON.stringify(editing_session_key)}`);
// TODO: Distinguish this case from a long disconnected browser tab waking up
}
const newFile = await apiService.get<Stream>({
url,
@@ -122,47 +114,30 @@ class DriveService implements IDriveService {
const form = new FormData();
const filename = encodeURIComponent(originalFile.last_version_cache.file_metadata.name);
form.append('file', newFile);
form.append('file', newFile, {
filename,
});
logger.info('Saving file version to Twake Drive: ', filename);
const queryString = keepEditing ? '?keepEditing=true' : '';
logger.info(`Saving file version to Twake Drive`, { editing_session_key, url });
await apiService.post({
url: `/internal/services/documents/v1/editing_session/${encodeURIComponent(editing_session_key)}` + queryString,
url: makeEditingSessionItemUrl(editing_session_key, {
keepEditing: keepEditing ? 'true' : null,
}),
payload: form,
token: user_token,
headers: form.getHeaders(),
});
} catch (error) {
logger.error('Failed to end editing session: ', error.stack);
throw error;
//TODO make monitoring for such kind of errors
if (error.response?.status === 404) {
logger.error('Forgotten OO document that Twake Drive doesnt know the key of.', { editing_session_key, url });
throw new UnknownKeyInDriveError(`Unknown key ${JSON.stringify(editing_session_key)}`);
//TODO: Distinguish this case from a long disconnected browser tab waking up
//TODO make monitoring for such kind of errors
} else {
logger.error('Failed to end editing session: ', error.stack);
throw error;
}
}
}
/**
* Get the document information by the editing session key. Just simple call to the drive API
* /item/editing_session/${editing_session_key}
* @param params
* @returns null if the key was not found, or the api response body
*/
public getByEditingSessionKey = async (params: { editing_session_key: string; user_token?: string }): Promise<DriveFileType['item']> => {
try {
const { editing_session_key } = params;
return await apiService.get<DriveFileType['item']>({
url: `/internal/services/documents/v1/editing_session/${encodeURIComponent(editing_session_key)}`,
token: params.user_token,
});
} catch (error) {
if (error?.response?.status === 404) return null;
throw error;
}
};
}
export default new DriveService();
@@ -16,7 +16,7 @@ export type QueryParams = { [key: string]: string | number };
export function joinURL(path: string[], params?: QueryParams) {
let joinedPath = path.map(x => x.replace(/(?:^\/+)+|(?:\/+$)/g, '')).join('/');
if (path[path.length - 1].endsWith('/')) joinedPath += '/';
const paramEntries = Object.entries(params || {});
const paramEntries = Object.entries(params || {}).filter(([, value]) => value !== undefined && value !== null);
if (paramEntries.length === 0) return joinedPath;
const query = paramEntries.map(p => p.map(encodeURIComponent).join('=')).join('&');
return joinedPath + (joinedPath.indexOf('?') > -1 ? '&' : '?') + query;