Merge branch 'main' into release/v1.0.4

This commit is contained in:
ericlinagora
2024-10-15 21:48:02 +02:00
committed by GitHub
60 changed files with 3271 additions and 3291 deletions
@@ -4,18 +4,15 @@ import parseYargsCommaSeparatedStringArray from "../../utils/yargs-comma-array";
import { NonPlatformCommandYargsBuilder } from "../../utils/non-plaform-command-yargs-builder";
import { openWithSystemViewer } from "../../../utils/exec";
const CORE_LANGUAGES = "en fr ru vn".split(" ");
// const iso639ToTwakeDriveISO = set1 => (set1 === "vi" ? "vn" : set1);
const twakeDriveISOToISO639 = twakeLang => (twakeLang === "vn" ? "vi" : twakeLang);
const CORE_LANGUAGES = "en fr ru vi".split(" ");
const TEMPLATE_VAR_NAME = "TDCLI_TRANSLATOR_URL";
const urlToTranslate = (text: string, to: string, from?: string) => {
const template = process.env[TEMPLATE_VAR_NAME];
if (!template) throw new Error(`${TEMPLATE_VAR_NAME} environment variable must be set.`);
const variables = {
to: twakeDriveISOToISO639(to),
from: from ? twakeDriveISOToISO639(from) : from,
to,
from,
text,
};
return template.replace(/%\{([^}:]+)(?::([^}]+))?\}/g, (_, varName, absentValue) => {
@@ -94,6 +94,10 @@ async function report(platform: TdrivePlatform, args: ListArguments) {
` - ${formatTS(version.date_added)} by ${await formatUser(version.creator_id)}`,
);
console.error(` - id: ${version.id}`);
if (dfile.name != version.filename)
console.error(` - filename: ${JSON.stringify(version.filename)}`);
if (dfile.name != version.file_metadata.name)
console.error(` - meta.name: ${JSON.stringify(version.file_metadata.name)}`);
console.error(
` - size: ${version.file_metadata.size} (${
version.file_metadata.size > previousSize ? "+" : ""
@@ -160,6 +160,17 @@ export class CrudException extends Error {
}
export interface Paginable {
/**
* In page token can be different type of data depending of the source.
* For ES it will be exactly page token, identifier of the next page.
* For PostgreSQL it will be number of the next page.
* For MongoDB it will be offset.
* This information is relevant for the Service/Repository level, in the controller
* we have only offset there for every type of source because in the "browse" controller
* we do not pass "next_page_token" to frontend and hence we are calculating
* offset on the frontend side and then passing offset to DocumentService.
*
*/
page_token?: string;
limitStr?: string;
reversed?: boolean;
@@ -3,7 +3,7 @@ import { Connector, UpsertOptions } from ".";
import { ConnectionOptions, DatabaseType } from "../..";
import { FindOptions } from "../repository/repository";
import { ColumnDefinition, EntityDefinition } from "../types";
import { ListResult } from "../../../../../framework/api/crud-service";
import { ListResult, Paginable, Pagination } from "../../../../../framework/api/crud-service";
export abstract class AbstractConnector<T extends ConnectionOptions> implements Connector {
constructor(protected type: DatabaseType, protected options: T, protected secret: string) {}
@@ -38,6 +38,8 @@ export abstract class AbstractConnector<T extends ConnectionOptions> implements
options: FindOptions,
): Promise<ListResult<EntityType>>;
abstract getOffsetPagination(options: Paginable): Pagination;
getOptions(): T {
return this.options;
}
@@ -3,7 +3,7 @@ import { DatabaseType } from "../..";
import { MongoConnectionOptions } from "./mongodb/mongodb";
import { ColumnDefinition, EntityDefinition } from "../types";
import { FindOptions } from "../repository/repository";
import { ListResult } from "../../../../../framework/api/crud-service";
import { ListResult, Paginable, Pagination } from "../../../../../framework/api/crud-service";
import { PostgresConnectionOptions } from "./postgres/postgres";
export * from "./mongodb/mongodb";
@@ -92,6 +92,13 @@ export interface Connector extends Initializable {
filters: any,
options: FindOptions,
): Promise<ListResult<EntityType>>;
/**
* Get the pagination for the given options where the pagination is offset based
* @param options Paginable
* @returns Pagination
*/
getOffsetPagination(options: Paginable): Pagination;
}
export declare type ConnectionOptions = MongoConnectionOptions | PostgresConnectionOptions;
@@ -316,4 +316,9 @@ export class MongoConnector extends AbstractConnector<MongoConnectionOptions> {
const nextPage: Paginable = new Pagination(nextToken, options.pagination.limitStr || "100");
return new ListResult<Table>(entityDefinition.type, entities, nextPage);
}
getOffsetPagination(options: Paginable): Pagination {
const { page_token, limitStr } = options;
return new Pagination(`${page_token}`, `${limitStr}`, options.reversed);
}
}
@@ -358,6 +358,12 @@ export class PostgresConnector extends AbstractConnector<PostgresConnectionOptio
throw err;
}
}
getOffsetPagination(options: Paginable): Pagination {
const { page_token, limitStr } = options;
const pageNumber = parseInt(page_token) / parseInt(limitStr);
return new Pagination(`${pageNumber}`, `${limitStr}`, options.reversed);
}
}
export type TableRowInfo = {
@@ -79,6 +79,9 @@ export class DocumentsService {
defaultQuota: number = config.has("drive.defaultUserQuota")
? config.get("drive.defaultUserQuota")
: 0;
manageAccessEnabled: boolean = config.has("drive.featureManageAccess")
? config.get("drive.featureManageAccess")
: false;
logger: TdriveLogger = getLogger("Documents Service");
async init(): Promise<this> {
@@ -222,18 +225,12 @@ export class DocumentsService {
if (options?.sort) {
sortField = this.getSortFieldMapping(options.sort);
}
const dbType = await globalResolver.database.getConnector().getType();
// Initialize pagination
let pagination;
if (options?.pagination) {
const { page_token, limitStr } = options.pagination;
const pageNumber =
dbType === "mongodb" ? parseInt(page_token) : parseInt(page_token) / parseInt(limitStr);
pagination = new Pagination(`${pageNumber}`, `${limitStr}`, false);
}
if (options?.pagination)
pagination = globalResolver.database.getConnector().getOffsetPagination(options.pagination);
let children = isDirectory
? (
@@ -562,33 +559,38 @@ export class DocumentsService {
oldParent = item.parent_id;
}
if (key === "access_info") {
const sharedWith = content.access_info.entities.filter(
info =>
info.type === "user" &&
info.id !== context.user.id &&
!item.access_info.entities.find(entity => entity.id === info.id),
);
// if manage access is disabled, we don't allow changing access level
if (!this.manageAccessEnabled) {
delete content.access_info;
} else if (content.access_info) {
const sharedWith = content.access_info.entities.filter(
info =>
info.type === "user" &&
info.id !== context.user.id &&
!item.access_info.entities.find(entity => entity.id === info.id),
);
item.access_info = content.access_info;
item.access_info = content.access_info;
if (sharedWith.length > 0) {
// Notify the user that the document has been shared with them
this.logger.info("Notifying user that the document has been shared with them: ", {
sharedWith,
});
gr.services.documents.engine.notifyDocumentShared({
context,
item,
notificationEmitter: context.user.id,
notificationReceiver: sharedWith[0].id,
if (sharedWith.length > 0) {
// Notify the user that the document has been shared with them
this.logger.info("Notifying user that the document has been shared with them: ", {
sharedWith,
});
gr.services.documents.engine.notifyDocumentShared({
context,
item,
notificationEmitter: context.user.id,
notificationReceiver: sharedWith[0].id,
});
}
item.access_info.entities.forEach(info => {
if (!info.grantor) {
info.grantor = context.user.id;
}
});
}
item.access_info.entities.forEach(info => {
if (!info.grantor) {
info.grantor = context.user.id;
}
});
} else if (key === "name") {
renamedTo = item.name = await getItemName(
content.parent_id || item.parent_id,
@@ -1148,7 +1150,15 @@ export class DocumentsService {
}
if (file) {
const fileEntity = await globalResolver.services.files.save(null, file, options, context);
const fileEntity = await globalResolver.services.files.save(
null,
file,
{
...options,
filename: options.filename ?? driveFile.name,
},
context,
);
await globalResolver.services.documents.documents.createVersion(
driveFile.id,
@@ -1158,6 +1168,7 @@ export class DocumentsService {
file_metadata: {
external_id: fileEntity.id,
source: "internal",
name: file.filename ?? driveFile.name,
},
},
context,
@@ -1184,8 +1195,14 @@ export class DocumentsService {
)}`,
);
} catch (error) {
logger.error({ error: `${error}` }, "Failed to cancel editing Drive item");
CrudException.throwMe(error, new CrudException("Failed to cancel editing Drive item", 500));
logger.error(
{ error: `${error}` },
`Failed to ${keepEditing ? "update" : "end"} editing Drive item`,
);
CrudException.throwMe(
error,
new CrudException(`Failed to ${keepEditing ? "update" : "end"} editing Drive item`, 500),
);
}
}
};
@@ -344,6 +344,7 @@ export const addDriveItemToArchive = async (
if (item.is_in_trash) return;
if (!item.is_directory) {
// random comment
const file_id = item.last_version_cache.file_metadata.external_id;
const file = await gr.services.files.download(file_id, context);
@@ -24,6 +24,7 @@ import {
import { DriveFileDTO } from "../dto/drive-file-dto";
import { DriveFileDTOBuilder } from "../../services/drive-file-dto-builder";
import config from "config";
import { formatAttachmentContentDispositionHeader } from "../../../files/utils";
export class DocumentsController {
private driveFileDTOBuilder = new DriveFileDTOBuilder();
@@ -421,7 +422,7 @@ export class DocumentsController {
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,
filename: q.filename || undefined,
type: q.resumableType || q.type || file?.mimetype || undefined,
waitForThumbnail: !!q.thumbnail_sync,
ignoreThumbnails: false,
@@ -510,9 +511,9 @@ export class DocumentsController {
return response;
} else if (archiveOrFile.file) {
const data = archiveOrFile.file;
const filename = encodeURIComponent(data.name.replace(/[^\p{L}0-9 _.-]/gu, ""));
response.header("Content-disposition", `attachment; filename="${filename}"`);
response.header("Content-Disposition", formatAttachmentContentDispositionHeader(data.name));
if (data.size) response.header("Content-Length", data.size);
response.type(data.mime);
return response.send(data.file);
@@ -565,7 +566,10 @@ export class DocumentsController {
try {
const archive = await globalResolver.services.documents.documents.createZip(ids, context);
reply.raw.setHeader("content-disposition", 'attachment; filename="twake_drive.zip"');
reply.raw.setHeader(
"content-disposition",
formatAttachmentContentDispositionHeader("twake_drive.zip"),
);
archive.on("finish", () => {
reply.status(200);
@@ -22,3 +22,12 @@ export const fileIsMedia = (file: Partial<File>): boolean => {
file.metadata?.mime?.startsWith("image/")
);
};
/**
* Generate RFC 5987 UTF-8 encoding compliant header value for `Content-Disposition`
* to make a browser download a reponse to a file with the provided name
*/
export const formatAttachmentContentDispositionHeader = (filename: string) => {
const encoded = encodeURIComponent(filename.replace(/[^\p{L}0-9 _.-]/gu, ""));
return `attachment; filename="${encoded}"; filename*=UTF-8''${encoded}`;
};
@@ -5,6 +5,7 @@ import { CompanyExecutionContext } from "../types";
import { UploadOptions } from "../../types";
import { PublicFile } from "../../entities/file";
import gr from "../../../global-resolver";
import { formatAttachmentContentDispositionHeader } from "../../utils";
export class FileController {
async save(
@@ -47,9 +48,8 @@ export class FileController {
const params = request.params;
try {
const data = await gr.services.files.download(params.id, context);
const filename = data.name.replace(/[^a-zA-Z0-9 -_.]/g, "");
response.header("Content-Disposition", formatAttachmentContentDispositionHeader(data.name));
response.header("Content-disposition", `attachment; filename="${filename}"`);
if (data.size) response.header("Content-Length", data.size);
response.type(data.mime);
return response.send(data.file);
@@ -2,5 +2,5 @@ import { Languages } from "./types";
export const languages: Languages = {
default: "en",
availables: ["en", "fr", "es", "vn", "ru"],
availables: ["en", "fr", "es", "vi", "ru"],
};
@@ -54,10 +54,6 @@ export function formatCompany(
[CompanyFeaturesEnum.CHAT_EDIT_FILES]: true,
[CompanyFeaturesEnum.CHAT_UNLIMITED_STORAGE]: true,
[CompanyFeaturesEnum.COMPANY_INVITE_MEMBER]: true,
// use the config value for this one
[CompanyFeaturesEnum.COMPANY_SEARCH_USERS]: JSON.parse(
config.get("drive.featureSearchUsers") || "true",
),
[CompanyFeaturesEnum.COMPANY_SHARED_DRIVE]: JSON.parse(
config.get("drive.featureSharedDrive") || "true",
),
@@ -67,6 +63,9 @@ export function formatCompany(
[CompanyFeaturesEnum.COMPANY_USER_QUOTA]: JSON.parse(
config.get("drive.featureUserQuota") || "false",
),
[CompanyFeaturesEnum.COMPANY_MANAGE_ACCESS]: JSON.parse(
config.get("drive.featureManageAccess") || "true",
),
},
{
...(res.plan?.features || {}),
@@ -76,8 +75,6 @@ export function formatCompany(
},
);
console.log("🚀🚀 res.plan.features: ", res.plan.features);
return res;
}
@@ -91,10 +91,10 @@ export const companyObjectSchema = {
[CompanyFeaturesEnum.CHAT_MULTIPLE_WORKSPACES]: { type: "boolean" },
[CompanyFeaturesEnum.CHAT_UNLIMITED_STORAGE]: { type: "boolean" },
[CompanyFeaturesEnum.COMPANY_INVITE_MEMBER]: { type: "boolean" },
[CompanyFeaturesEnum.COMPANY_SEARCH_USERS]: { type: "boolean" },
[CompanyFeaturesEnum.COMPANY_SHARED_DRIVE]: { type: "boolean" },
[CompanyFeaturesEnum.COMPANY_DISPLAY_EMAIL]: { type: "boolean" },
[CompanyFeaturesEnum.COMPANY_USER_QUOTA]: { type: "boolean" },
[CompanyFeaturesEnum.COMPANY_MANAGE_ACCESS]: { type: "boolean" },
guests: { type: "number" }, // to rename or delete
members: { type: "number" }, // to rename or delete
storage: { type: "number" }, // to rename or delete
@@ -83,10 +83,10 @@ export enum CompanyFeaturesEnum {
CHAT_EDIT_FILES = "chat:edit_files",
CHAT_UNLIMITED_STORAGE = "chat:unlimited_storage",
COMPANY_INVITE_MEMBER = "company:invite_member",
COMPANY_SEARCH_USERS = "company:search_users",
COMPANY_SHARED_DRIVE = "company:shared_drive",
COMPANY_DISPLAY_EMAIL = "company:display_email",
COMPANY_USER_QUOTA = "company:user_quota",
COMPANY_MANAGE_ACCESS = "company:managed_access",
}
export type CompanyFeaturesObject = {
@@ -96,10 +96,10 @@ export type CompanyFeaturesObject = {
[CompanyFeaturesEnum.CHAT_EDIT_FILES]?: boolean;
[CompanyFeaturesEnum.CHAT_UNLIMITED_STORAGE]?: boolean;
[CompanyFeaturesEnum.COMPANY_INVITE_MEMBER]?: boolean;
[CompanyFeaturesEnum.COMPANY_SEARCH_USERS]?: boolean;
[CompanyFeaturesEnum.COMPANY_SHARED_DRIVE]?: boolean;
[CompanyFeaturesEnum.COMPANY_DISPLAY_EMAIL]?: boolean;
[CompanyFeaturesEnum.COMPANY_USER_QUOTA]?: boolean;
[CompanyFeaturesEnum.COMPANY_MANAGE_ACCESS]?: boolean;
};
export type CompanyLimitsObject = {