diff --git a/tdrive/connectors/onlyoffice-connector/src/interfaces/drive.interface.ts b/tdrive/connectors/onlyoffice-connector/src/interfaces/drive.interface.ts index d3195931..9755461c 100644 --- a/tdrive/connectors/onlyoffice-connector/src/interfaces/drive.interface.ts +++ b/tdrive/connectors/onlyoffice-connector/src/interfaces/drive.interface.ts @@ -23,5 +23,4 @@ export interface IDriveService { beginEditingSession: (company_id: string, drive_file_id: string) => Promise; addEditingSessionVersion: (editing_session_key: string, url: string) => Promise; endEditing: (editing_session_key: string, url: string) => Promise; - getByEditingSessionKey: (params: { company_id: string; editing_session_key: string; user_token?: string }) => Promise; } diff --git a/tdrive/connectors/onlyoffice-connector/src/services/api.service.ts b/tdrive/connectors/onlyoffice-connector/src/services/api.service.ts index e563a373..4d26860e 100644 --- a/tdrive/connectors/onlyoffice-connector/src/services/api.service.ts +++ b/tdrive/connectors/onlyoffice-connector/src/services/api.service.ts @@ -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; } }; diff --git a/tdrive/connectors/onlyoffice-connector/src/services/drive.service.ts b/tdrive/connectors/onlyoffice-connector/src/services/drive.service.ts index 2f8ed3ed..c006991b 100644 --- a/tdrive/connectors/onlyoffice-connector/src/services/drive.service.ts +++ b/tdrive/connectors/onlyoffice-connector/src/services/drive.service.ts @@ -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({ - 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({ 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 => { - try { - const { editing_session_key } = params; - return await apiService.get({ - 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(); diff --git a/tdrive/connectors/onlyoffice-connector/src/utils.ts b/tdrive/connectors/onlyoffice-connector/src/utils.ts index 1fc1d55b..36b73140 100644 --- a/tdrive/connectors/onlyoffice-connector/src/utils.ts +++ b/tdrive/connectors/onlyoffice-connector/src/utils.ts @@ -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;