✨Added support of the session editing key for the OnlyOffice connector
This commit is contained in:
committed by
Eric Doughty-Papassideris
parent
b6be90f825
commit
c66123f450
@@ -61,6 +61,8 @@ import archiver from "archiver";
|
||||
import internal from "stream";
|
||||
import config from "config";
|
||||
import { randomUUID } from "crypto";
|
||||
import { MultipartFile } from "@fastify/multipart";
|
||||
import { UploadOptions } from "src/services/files/types";
|
||||
|
||||
export class DocumentsService {
|
||||
version: "1";
|
||||
@@ -1030,13 +1032,15 @@ export class DocumentsService {
|
||||
* End editing session either by providing a URL to a new file to create a version,
|
||||
* or not, to just cancel the session.
|
||||
* @param editing_session_key Editing key of the DriveFile
|
||||
* @param newFileUrl Optional URL to a new version of the file to store
|
||||
* @param file Multipart files from the incoming http request
|
||||
* @param options Optional upload information from the request
|
||||
* @param context
|
||||
*/
|
||||
endEditing = async (
|
||||
editing_session_key: string,
|
||||
newFileUrl: string | null,
|
||||
context: DriveExecutionContext,
|
||||
file: MultipartFile,
|
||||
options: UploadOptions,
|
||||
context: CompanyExecutionContext,
|
||||
) => {
|
||||
if (!context) {
|
||||
this.logger.error("invalid execution context");
|
||||
@@ -1062,19 +1066,21 @@ export class DocumentsService {
|
||||
);
|
||||
}
|
||||
|
||||
if (newFileUrl) {
|
||||
// const response = await axios.request({
|
||||
// url: domain + req.url,
|
||||
// method: req.method as any,
|
||||
// headers: _.omit(req.headers, "host", "content-length") as {
|
||||
// [key: string]: string;
|
||||
// },
|
||||
// data: req.body as any,
|
||||
// responseType: "stream",
|
||||
// maxRedirects: 0,
|
||||
// validateStatus: null,
|
||||
// });
|
||||
//TODO: save file into new version
|
||||
if (file) {
|
||||
const fileEntity = await globalResolver.services.files.save(null, file, options, context);
|
||||
|
||||
await globalResolver.services.documents.documents.createVersion(
|
||||
driveFile.id,
|
||||
{
|
||||
drive_item_id: driveFile.id,
|
||||
provider: "internal",
|
||||
file_metadata: {
|
||||
external_id: fileEntity.id,
|
||||
source: "internal",
|
||||
},
|
||||
},
|
||||
context,
|
||||
);
|
||||
}
|
||||
|
||||
try {
|
||||
@@ -1092,7 +1098,6 @@ export class DocumentsService {
|
||||
result.currentValue,
|
||||
)}`,
|
||||
);
|
||||
return { ok: true };
|
||||
} catch (error) {
|
||||
logger.error({ error: `${error}` }, "Failed to cancel editing Drive item");
|
||||
CrudException.throwMe(error, new CrudException("Failed to cancel editing Drive item", 500));
|
||||
|
||||
@@ -392,6 +392,7 @@ export class DocumentsController {
|
||||
return await globalResolver.services.documents.documents.endEditing(
|
||||
editing_session_key,
|
||||
null,
|
||||
null,
|
||||
context,
|
||||
);
|
||||
} catch (error) {
|
||||
@@ -406,10 +407,44 @@ export class DocumentsController {
|
||||
endEditing = async (
|
||||
request: FastifyRequest<{
|
||||
Params: ItemRequestByEditingSessionKeyParams;
|
||||
Body: { editorApplicationId: string };
|
||||
Querystring: Record<string, string>;
|
||||
Body: {
|
||||
item: Partial<DriveFile>;
|
||||
version: Partial<FileVersion>;
|
||||
};
|
||||
}>,
|
||||
) => {
|
||||
console.log(request); // make linter happy
|
||||
const { editing_session_key } = request.params;
|
||||
if (!editing_session_key) throw new CrudException("Editing session key must be set", 400);
|
||||
|
||||
const context = getDriveExecutionContext(request);
|
||||
|
||||
if (request.isMultipart()) {
|
||||
const file = await request.file();
|
||||
const q: Record<string, string> = request.query;
|
||||
const options: UploadOptions = {
|
||||
totalChunks: parseInt(q.resumableTotalChunks || q.total_chunks) || 1,
|
||||
totalSize: parseInt(q.resumableTotalSize || q.total_size) || 0,
|
||||
chunkNumber: parseInt(q.resumableChunkNumber || q.chunk_number) || 1,
|
||||
filename: q.resumableFilename || q.filename || file?.filename || undefined,
|
||||
type: q.resumableType || q.type || file?.mimetype || undefined,
|
||||
waitForThumbnail: !!q.thumbnail_sync,
|
||||
ignoreThumbnails: false,
|
||||
};
|
||||
return await globalResolver.services.documents.documents.endEditing(
|
||||
editing_session_key,
|
||||
file,
|
||||
options,
|
||||
context,
|
||||
);
|
||||
} else {
|
||||
return await globalResolver.services.documents.documents.endEditing(
|
||||
editing_session_key,
|
||||
null,
|
||||
null,
|
||||
context,
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
downloadGetToken = async (
|
||||
|
||||
@@ -105,7 +105,7 @@ const routes: FastifyPluginCallback = (fastify: FastifyInstance, _options, next)
|
||||
method: "DELETE",
|
||||
url: `${serviceUrl}/editing_session/:editing_session_key`,
|
||||
preValidation: [fastify.authenticateOptional],
|
||||
handler: documentsController.endEditing.bind(documentsController),
|
||||
handler: documentsController.cancelEditing.bind(documentsController),
|
||||
});
|
||||
|
||||
fastify.route({
|
||||
|
||||
@@ -409,6 +409,36 @@ export default class UserApi {
|
||||
});
|
||||
}
|
||||
|
||||
async endEditingDocument(
|
||||
editingSessionKey: string
|
||||
): Promise<Response> {
|
||||
const fullPath = `${__dirname}/assets/${UserApi.ALL_FILES[0]}`;
|
||||
const readable= Readable.from(fs.createReadStream(fullPath));
|
||||
const form = formAutoContent({ file: readable });
|
||||
form.headers["authorization"] = `Bearer ${this.jwt}`;
|
||||
|
||||
return await this.platform.app.inject({
|
||||
method: "POST",
|
||||
url: `${UserApi.DOC_URL}/companies/${this.platform.workspace.company_id}/item/editing_session/${editingSessionKey}`,
|
||||
headers: {
|
||||
authorization: `Bearer ${this.jwt}`
|
||||
},
|
||||
...form,
|
||||
});
|
||||
}
|
||||
|
||||
async cancelEditingDocument(
|
||||
editingSessionKey: string,
|
||||
): Promise<Response> {
|
||||
return await this.platform.app.inject({
|
||||
method: "DELETE",
|
||||
url: `${UserApi.DOC_URL}/companies/${this.platform.workspace.company_id}/item/editing_session/${editingSessionKey}`,
|
||||
headers: {
|
||||
authorization: `Bearer ${this.jwt}`
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
async beginEditingDocumentExpectOk(
|
||||
driveFileId: string,
|
||||
editorApplicationId: string,
|
||||
|
||||
@@ -0,0 +1,52 @@
|
||||
// import { OidcJwtVerifier } from "../../../src/services/console/clients/remote-jwks-verifier";
|
||||
//
|
||||
// export class UserAuthorization {
|
||||
// /**
|
||||
// * Just send the login requests without any validation and login response assertion
|
||||
// */
|
||||
// public async login(session?: string) {
|
||||
// if (session !== undefined) {
|
||||
// this.session = session;
|
||||
// } else {
|
||||
// this.session = uuidv1();
|
||||
// }
|
||||
// const payload = {
|
||||
// claims: {
|
||||
// sub: this.user.id,
|
||||
// first_name: this.user.first_name,
|
||||
// sid: this.session,
|
||||
// },
|
||||
// };
|
||||
// const verifierMock = jest.spyOn(OidcJwtVerifier.prototype, "verifyIdToken");
|
||||
// verifierMock.mockImplementation(() => {
|
||||
// return Promise.resolve(payload); // Return the predefined payload
|
||||
// });
|
||||
// return await this.api.post("/internal/services/console/v1/login", {
|
||||
// oidc_id_token: "sample_oidc_token",
|
||||
// });
|
||||
// }
|
||||
//
|
||||
// public async logout() {
|
||||
// const payload = {
|
||||
// claims: {
|
||||
// iss: "tdrive_lemonldap",
|
||||
// sub: this.user.id,
|
||||
// sid: this.session,
|
||||
// aud: "your-audience",
|
||||
// iat: Math.floor(Date.now() / 1000),
|
||||
// jti: "jwt-id",
|
||||
// events: {
|
||||
// "http://schemas.openid.net/event/backchannel-logout": {},
|
||||
// },
|
||||
// }
|
||||
// };
|
||||
// const verifierMock = jest.spyOn(OidcJwtVerifier.prototype, "verifyLogoutToken");
|
||||
// verifierMock.mockImplementation(() => {
|
||||
// return Promise.resolve(payload); // Return the predefined payload
|
||||
// });
|
||||
//
|
||||
// return await this.api.post("/internal/services/console/v1/backchannel_logout", {
|
||||
// logout_token: "logout_token_rsa256",
|
||||
// });
|
||||
// }
|
||||
// }
|
||||
@@ -4,6 +4,7 @@ import { init, TestPlatform } from "../setup";
|
||||
import UserApi from "../common/user-api";
|
||||
|
||||
import { DriveFile, TYPE as DriveFileType } from "../../../src/services/documents/entities/drive-file";
|
||||
import exp = require("node:constants");
|
||||
|
||||
describe("the Drive's documents' editing session kind-of-lock", () => {
|
||||
let platform: TestPlatform | null;
|
||||
@@ -110,8 +111,28 @@ describe("the Drive's documents' editing session kind-of-lock", () => {
|
||||
expect(temporaryDocument.id).toBe(foundDocumentResult.json().id);
|
||||
});
|
||||
|
||||
it('can end an editing session on a document only once with the right key', async () => {
|
||||
it('can cancel an editing session on a document only once with the right key', async () => {
|
||||
//given
|
||||
const editingSessionKey = await currentUser.beginEditingDocumentExpectOk(temporaryDocument.id, 'e2e_testing');
|
||||
|
||||
//when
|
||||
const response = await currentUser.cancelEditingDocument(editingSessionKey);
|
||||
//then
|
||||
expect(response.statusCode).toBe(200);
|
||||
const newSessionKey = await currentUser.beginEditingDocumentExpectOk(temporaryDocument.id, 'e2e_testing');
|
||||
expect(newSessionKey).not.toEqual(editingSessionKey);
|
||||
});
|
||||
|
||||
it('can end editing with a new version of document', async () => {
|
||||
//given
|
||||
const editingSessionKey = await currentUser.beginEditingDocumentExpectOk(temporaryDocument.id, 'e2e_testing');
|
||||
//when
|
||||
const response = await currentUser.endEditingDocument(editingSessionKey);
|
||||
|
||||
//then
|
||||
expect(response.statusCode).toBe(200);
|
||||
const document = await currentUser.getDocumentOKCheck(temporaryDocument.id);
|
||||
expect(document.versions.length).toEqual(2);
|
||||
});
|
||||
|
||||
|
||||
});
|
||||
|
||||
@@ -82,28 +82,9 @@ class OnlyOfficeController {
|
||||
break;
|
||||
|
||||
case OnlyOffice.Callback.Status.READY_FOR_SAVING:
|
||||
const driveFile = await driveService.getByEditingSessionKey({
|
||||
company_id,
|
||||
editing_session_key,
|
||||
});
|
||||
if (!driveFile) {
|
||||
throw new Error('Error getting drive files ');
|
||||
}
|
||||
|
||||
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);
|
||||
await driveService.endEditing(company_id, editing_session_key, url);
|
||||
|
||||
logger.info(`New version for session ${editing_session_key} created`);
|
||||
return respondToOO();
|
||||
|
||||
case OnlyOffice.Callback.Status.CLOSED_WITHOUT_CHANGES:
|
||||
|
||||
@@ -6,6 +6,7 @@ export type DriveFileType = {
|
||||
date_added: number;
|
||||
file_metadata: {
|
||||
external_id: string;
|
||||
name?: string;
|
||||
};
|
||||
};
|
||||
};
|
||||
@@ -20,6 +21,6 @@ export interface IDriveService {
|
||||
get: (params: DriveRequestParams) => Promise<DriveFileType>;
|
||||
createVersion: (params: { company_id: string; drive_file_id: string; file_id: string }) => Promise<DriveFileType['item']['last_version_cache']>;
|
||||
beginEditingSession: (company_id: string, drive_file_id: string) => Promise<string>;
|
||||
endEditing: (company_id: string, editing_session_key: string) => Promise<void>;
|
||||
getByEditingSessionKey: (params: { company_id: string; editing_session_key: string; user_token?: string }) => Promise<DriveFileType>;
|
||||
endEditing: (company_id: string, editing_session_key: string, file_source_url: string) => Promise<void>;
|
||||
getByEditingSessionKey: (params: { company_id: string; editing_session_key: string; user_token?: string }) => Promise<DriveFileType['item']>;
|
||||
}
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
import { DriveFileType, IDriveService } from '@/interfaces/drive.interface';
|
||||
import apiService from './api.service';
|
||||
import logger from '../lib/logger';
|
||||
import { Stream } from 'stream';
|
||||
import FormData from 'form-data';
|
||||
|
||||
/**
|
||||
* Client for Twake Drive's APIs dealing with `DriveItem`s, using {@see apiService}
|
||||
@@ -30,7 +32,7 @@ class DriveService implements IDriveService {
|
||||
}): Promise<DriveFileType['item']['last_version_cache']> => {
|
||||
try {
|
||||
const { company_id, drive_file_id, file_id } = params;
|
||||
const resource = await apiService.post<{}, DriveFileType['item']['last_version_cache']>({
|
||||
return await apiService.post<{}, DriveFileType['item']['last_version_cache']>({
|
||||
url: `/internal/services/documents/v1/companies/${company_id}/item/${drive_file_id}/version`,
|
||||
payload: {
|
||||
drive_item_id: drive_file_id,
|
||||
@@ -41,8 +43,6 @@ class DriveService implements IDriveService {
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
return resource;
|
||||
} catch (error) {
|
||||
logger.error('Failed to create version: ', error.stack);
|
||||
return Promise.reject();
|
||||
@@ -68,10 +68,49 @@ class DriveService implements IDriveService {
|
||||
}
|
||||
}
|
||||
|
||||
public async endEditing(company_id: string, editing_session_key: string) {
|
||||
public async cancelEditing(company_id: string, editing_session_key) {
|
||||
try {
|
||||
await apiService.delete<{}>({
|
||||
url: `/internal/services/documents/v1/companies/${company_id}/item/editing_session/${editing_session_key}`,
|
||||
url: `/internal/services/documents/v1/companies/${company_id}/item/editing_session/${encodeURIComponent(editing_session_key)}`,
|
||||
});
|
||||
} catch (error) {
|
||||
logger.error('Failed to begin editing session: ', error.stack);
|
||||
throw error;
|
||||
//TODO make monitoring for such kind of errors
|
||||
}
|
||||
}
|
||||
|
||||
public async endEditing(company_id: string, editing_session_key: string, url: string) {
|
||||
try {
|
||||
if (!url) {
|
||||
throw Error('no url found');
|
||||
}
|
||||
|
||||
const originalFile = await this.getByEditingSessionKey({ company_id, editing_session_key });
|
||||
|
||||
if (!originalFile) {
|
||||
throw Error('original file not found');
|
||||
}
|
||||
|
||||
const newFile = await apiService.get<Stream>({
|
||||
url,
|
||||
responseType: 'stream',
|
||||
});
|
||||
|
||||
const form = new FormData();
|
||||
|
||||
const filename = encodeURIComponent(originalFile.last_version_cache.file_metadata.name);
|
||||
|
||||
form.append('file', newFile, {
|
||||
filename,
|
||||
});
|
||||
|
||||
logger.info('Saving file version to Twake Drive: ', filename);
|
||||
|
||||
await apiService.post({
|
||||
url: `/internal/services/documents/v1/companies/${company_id}/item/editing_session/${encodeURIComponent(editing_session_key)}`,
|
||||
payload: form,
|
||||
headers: form.getHeaders(),
|
||||
});
|
||||
} catch (error) {
|
||||
logger.error('Failed to begin editing session: ', error.stack);
|
||||
@@ -89,11 +128,11 @@ class DriveService implements IDriveService {
|
||||
company_id: string;
|
||||
editing_session_key: string;
|
||||
user_token?: string;
|
||||
}): Promise<DriveFileType> => {
|
||||
}): Promise<DriveFileType['item']> => {
|
||||
try {
|
||||
const { company_id, editing_session_key } = params;
|
||||
return await apiService.get<DriveFileType>({
|
||||
url: `/internal/services/documents/v1/companies/${company_id}/item/editing_session/${editing_session_key}`,
|
||||
return await apiService.get<DriveFileType['item']>({
|
||||
url: `/internal/services/documents/v1/companies/${company_id}/item/editing_session/${encodeURIComponent(editing_session_key)}`,
|
||||
token: params.user_token,
|
||||
});
|
||||
} catch (error) {
|
||||
|
||||
Reference in New Issue
Block a user