Merge branch 'main' into release/v1.0.4
This commit is contained in:
@@ -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
-1
@@ -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;
|
||||
}
|
||||
|
||||
+8
-1
@@ -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;
|
||||
|
||||
+5
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
+6
@@ -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 = {
|
||||
|
||||
Reference in New Issue
Block a user