mirror of
https://github.com/docmost/docmost.git
synced 2026-05-07 14:43:06 +08:00
Compare commits
4 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 6f92ecedf6 | |||
| 7e16c39f9a | |||
| 03714a8bec | |||
| 3003befe07 |
@@ -170,6 +170,8 @@ const CommandGroups: SlashMenuGroupedItemsType = {
|
|||||||
input.type = "file";
|
input.type = "file";
|
||||||
input.accept = "image/*";
|
input.accept = "image/*";
|
||||||
input.multiple = true;
|
input.multiple = true;
|
||||||
|
input.style.display = "none";
|
||||||
|
document.body.appendChild(input);
|
||||||
input.onchange = async () => {
|
input.onchange = async () => {
|
||||||
if (input.files?.length) {
|
if (input.files?.length) {
|
||||||
for (const file of input.files) {
|
for (const file of input.files) {
|
||||||
@@ -179,8 +181,7 @@ const CommandGroups: SlashMenuGroupedItemsType = {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Reset the input value to allow uploading the same file again if needed
|
input.remove();
|
||||||
input.value = "";
|
|
||||||
};
|
};
|
||||||
input.click();
|
input.click();
|
||||||
},
|
},
|
||||||
@@ -202,6 +203,8 @@ const CommandGroups: SlashMenuGroupedItemsType = {
|
|||||||
input.type = "file";
|
input.type = "file";
|
||||||
input.accept = "video/*";
|
input.accept = "video/*";
|
||||||
input.multiple = true;
|
input.multiple = true;
|
||||||
|
input.style.display = "none";
|
||||||
|
document.body.appendChild(input);
|
||||||
input.onchange = async () => {
|
input.onchange = async () => {
|
||||||
if (input.files?.length) {
|
if (input.files?.length) {
|
||||||
for (const file of input.files) {
|
for (const file of input.files) {
|
||||||
@@ -211,8 +214,7 @@ const CommandGroups: SlashMenuGroupedItemsType = {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Reset the input value to allow uploading the same file again if needed
|
input.remove();
|
||||||
input.value = "";
|
|
||||||
};
|
};
|
||||||
input.click();
|
input.click();
|
||||||
},
|
},
|
||||||
@@ -234,6 +236,8 @@ const CommandGroups: SlashMenuGroupedItemsType = {
|
|||||||
input.type = "file";
|
input.type = "file";
|
||||||
input.accept = "";
|
input.accept = "";
|
||||||
input.multiple = true;
|
input.multiple = true;
|
||||||
|
input.style.display = "none";
|
||||||
|
document.body.appendChild(input);
|
||||||
input.onchange = async () => {
|
input.onchange = async () => {
|
||||||
if (input.files?.length) {
|
if (input.files?.length) {
|
||||||
for (const file of input.files) {
|
for (const file of input.files) {
|
||||||
@@ -243,8 +247,7 @@ const CommandGroups: SlashMenuGroupedItemsType = {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Reset the input value to allow uploading the same file again if needed
|
input.remove();
|
||||||
input.value = "";
|
|
||||||
};
|
};
|
||||||
input.click();
|
input.click();
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -157,7 +157,9 @@ export function TitleEditor({
|
|||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
titleEditor?.commands.focus("end");
|
// guard against Cannot access view['hasFocus'] error
|
||||||
|
if (!titleEditor?.isInitialized) return;
|
||||||
|
titleEditor?.commands?.focus("end");
|
||||||
}, 500);
|
}, 500);
|
||||||
}, [titleEditor]);
|
}, [titleEditor]);
|
||||||
|
|
||||||
|
|||||||
@@ -17,13 +17,13 @@ import {
|
|||||||
UseInterceptors,
|
UseInterceptors,
|
||||||
} from '@nestjs/common';
|
} from '@nestjs/common';
|
||||||
import { AttachmentService } from './services/attachment.service';
|
import { AttachmentService } from './services/attachment.service';
|
||||||
import { FastifyReply } from 'fastify';
|
import { FastifyReply, FastifyRequest } from 'fastify';
|
||||||
import { FileInterceptor } from '../../common/interceptors/file.interceptor';
|
import { FileInterceptor } from '../../common/interceptors/file.interceptor';
|
||||||
import * as bytes from 'bytes';
|
import * as bytes from 'bytes';
|
||||||
import { AuthUser } from '../../common/decorators/auth-user.decorator';
|
import { AuthUser } from '../../common/decorators/auth-user.decorator';
|
||||||
import { AuthWorkspace } from '../../common/decorators/auth-workspace.decorator';
|
import { AuthWorkspace } from '../../common/decorators/auth-workspace.decorator';
|
||||||
import { JwtAuthGuard } from '../../common/guards/jwt-auth.guard';
|
import { JwtAuthGuard } from '../../common/guards/jwt-auth.guard';
|
||||||
import { User, Workspace } from '@docmost/db/types/entity.types';
|
import { Attachment, User, Workspace } from '@docmost/db/types/entity.types';
|
||||||
import { StorageService } from '../../integrations/storage/storage.service';
|
import { StorageService } from '../../integrations/storage/storage.service';
|
||||||
import {
|
import {
|
||||||
getAttachmentFolderPath,
|
getAttachmentFolderPath,
|
||||||
@@ -151,6 +151,7 @@ export class AttachmentController {
|
|||||||
@UseGuards(JwtAuthGuard)
|
@UseGuards(JwtAuthGuard)
|
||||||
@Get('/files/:fileId/:fileName')
|
@Get('/files/:fileId/:fileName')
|
||||||
async getFile(
|
async getFile(
|
||||||
|
@Req() req: FastifyRequest,
|
||||||
@Res() res: FastifyReply,
|
@Res() res: FastifyReply,
|
||||||
@AuthUser() user: User,
|
@AuthUser() user: User,
|
||||||
@AuthWorkspace() workspace: Workspace,
|
@AuthWorkspace() workspace: Workspace,
|
||||||
@@ -181,22 +182,7 @@ export class AttachmentController {
|
|||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const fileStream = await this.storageService.readStream(
|
return await this.sendFileResponse(req, res, attachment, 'private');
|
||||||
attachment.filePath,
|
|
||||||
);
|
|
||||||
res.headers({
|
|
||||||
'Content-Type': attachment.mimeType,
|
|
||||||
'Cache-Control': 'private, max-age=3600',
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!inlineFileExtensions.includes(attachment.fileExt)) {
|
|
||||||
res.header(
|
|
||||||
'Content-Disposition',
|
|
||||||
`attachment; filename="${encodeURIComponent(attachment.fileName)}"`,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return res.send(fileStream);
|
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
this.logger.error(err);
|
this.logger.error(err);
|
||||||
throw new NotFoundException('File not found');
|
throw new NotFoundException('File not found');
|
||||||
@@ -205,6 +191,7 @@ export class AttachmentController {
|
|||||||
|
|
||||||
@Get('/files/public/:fileId/:fileName')
|
@Get('/files/public/:fileId/:fileName')
|
||||||
async getPublicFile(
|
async getPublicFile(
|
||||||
|
@Req() req: FastifyRequest,
|
||||||
@Res() res: FastifyReply,
|
@Res() res: FastifyReply,
|
||||||
@AuthWorkspace() workspace: Workspace,
|
@AuthWorkspace() workspace: Workspace,
|
||||||
@Param('fileId') fileId: string,
|
@Param('fileId') fileId: string,
|
||||||
@@ -243,22 +230,7 @@ export class AttachmentController {
|
|||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const fileStream = await this.storageService.readStream(
|
return await this.sendFileResponse(req, res, attachment, 'public');
|
||||||
attachment.filePath,
|
|
||||||
);
|
|
||||||
res.headers({
|
|
||||||
'Content-Type': attachment.mimeType,
|
|
||||||
'Cache-Control': 'public, max-age=3600',
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!inlineFileExtensions.includes(attachment.fileExt)) {
|
|
||||||
res.header(
|
|
||||||
'Content-Disposition',
|
|
||||||
`attachment; filename="${encodeURIComponent(attachment.fileName)}"`,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return res.send(fileStream);
|
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
this.logger.error(err);
|
this.logger.error(err);
|
||||||
throw new NotFoundException('File not found');
|
throw new NotFoundException('File not found');
|
||||||
@@ -433,4 +405,69 @@ export class AttachmentController {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private async sendFileResponse(
|
||||||
|
req: FastifyRequest,
|
||||||
|
res: FastifyReply,
|
||||||
|
attachment: Attachment,
|
||||||
|
cacheScope: 'private' | 'public',
|
||||||
|
) {
|
||||||
|
const fileSize = Number(attachment.fileSize);
|
||||||
|
const rangeHeader = req.headers.range;
|
||||||
|
|
||||||
|
res.header('Accept-Ranges', 'bytes');
|
||||||
|
|
||||||
|
if (!inlineFileExtensions.includes(attachment.fileExt)) {
|
||||||
|
res.header(
|
||||||
|
'Content-Disposition',
|
||||||
|
`attachment; filename="${encodeURIComponent(attachment.fileName)}"`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (rangeHeader && fileSize) {
|
||||||
|
const match = rangeHeader.match(/bytes=(\d+)-(\d*)/);
|
||||||
|
if (match) {
|
||||||
|
const start = parseInt(match[1], 10);
|
||||||
|
const end = match[2]
|
||||||
|
? Math.min(parseInt(match[2], 10), fileSize - 1)
|
||||||
|
: fileSize - 1;
|
||||||
|
|
||||||
|
if (start >= fileSize || start > end) {
|
||||||
|
res.status(416);
|
||||||
|
res.header('Content-Range', `bytes */${fileSize}`);
|
||||||
|
return res.send();
|
||||||
|
}
|
||||||
|
|
||||||
|
const fileStream = await this.storageService.readRangeStream(
|
||||||
|
attachment.filePath,
|
||||||
|
{ start, end },
|
||||||
|
);
|
||||||
|
|
||||||
|
res.status(206);
|
||||||
|
res.headers({
|
||||||
|
'Content-Type': attachment.mimeType,
|
||||||
|
'Content-Range': `bytes ${start}-${end}/${fileSize}`,
|
||||||
|
'Content-Length': end - start + 1,
|
||||||
|
'Cache-Control': `${cacheScope}, max-age=3600`,
|
||||||
|
});
|
||||||
|
|
||||||
|
return res.send(fileStream);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const fileStream = await this.storageService.readStream(
|
||||||
|
attachment.filePath,
|
||||||
|
);
|
||||||
|
|
||||||
|
res.headers({
|
||||||
|
'Content-Type': attachment.mimeType,
|
||||||
|
'Cache-Control': `${cacheScope}, max-age=3600`,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (fileSize) {
|
||||||
|
res.header('Content-Length', fileSize);
|
||||||
|
}
|
||||||
|
|
||||||
|
return res.send(fileStream);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -73,6 +73,20 @@ export class LocalDriver implements StorageDriver {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async readRangeStream(
|
||||||
|
filePath: string,
|
||||||
|
range: { start: number; end: number },
|
||||||
|
): Promise<Readable> {
|
||||||
|
try {
|
||||||
|
return createReadStream(this._fullPath(filePath), {
|
||||||
|
start: range.start,
|
||||||
|
end: range.end,
|
||||||
|
});
|
||||||
|
} catch (err) {
|
||||||
|
throw new Error(`Failed to read file: ${(err as Error).message}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
async exists(filePath: string): Promise<boolean> {
|
async exists(filePath: string): Promise<boolean> {
|
||||||
try {
|
try {
|
||||||
return await fs.pathExists(this._fullPath(filePath));
|
return await fs.pathExists(this._fullPath(filePath));
|
||||||
|
|||||||
@@ -130,6 +130,25 @@ export class S3Driver implements StorageDriver {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async readRangeStream(
|
||||||
|
filePath: string,
|
||||||
|
range: { start: number; end: number },
|
||||||
|
): Promise<Readable> {
|
||||||
|
try {
|
||||||
|
const command = new GetObjectCommand({
|
||||||
|
Bucket: this.config.bucket,
|
||||||
|
Key: filePath,
|
||||||
|
Range: `bytes=${range.start}-${range.end}`,
|
||||||
|
});
|
||||||
|
|
||||||
|
const response = await this.s3Client.send(command);
|
||||||
|
|
||||||
|
return response.Body as Readable;
|
||||||
|
} catch (err) {
|
||||||
|
throw new Error(`Failed to read file from S3: ${(err as Error).message}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
async exists(filePath: string): Promise<boolean> {
|
async exists(filePath: string): Promise<boolean> {
|
||||||
try {
|
try {
|
||||||
const command = new HeadObjectCommand({
|
const command = new HeadObjectCommand({
|
||||||
|
|||||||
@@ -11,6 +11,11 @@ export interface StorageDriver {
|
|||||||
|
|
||||||
readStream(filePath: string): Promise<Readable>;
|
readStream(filePath: string): Promise<Readable>;
|
||||||
|
|
||||||
|
readRangeStream(
|
||||||
|
filePath: string,
|
||||||
|
range: { start: number; end: number },
|
||||||
|
): Promise<Readable>;
|
||||||
|
|
||||||
exists(filePath: string): Promise<boolean>;
|
exists(filePath: string): Promise<boolean>;
|
||||||
|
|
||||||
getUrl(filePath: string): string;
|
getUrl(filePath: string): string;
|
||||||
|
|||||||
@@ -33,6 +33,13 @@ export class StorageService {
|
|||||||
return this.storageDriver.readStream(filePath);
|
return this.storageDriver.readStream(filePath);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async readRangeStream(
|
||||||
|
filePath: string,
|
||||||
|
range: { start: number; end: number },
|
||||||
|
): Promise<Readable> {
|
||||||
|
return this.storageDriver.readRangeStream(filePath, range);
|
||||||
|
}
|
||||||
|
|
||||||
async exists(filePath: string): Promise<boolean> {
|
async exists(filePath: string): Promise<boolean> {
|
||||||
return this.storageDriver.exists(filePath);
|
return this.storageDriver.exists(filePath);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,9 +1,9 @@
|
|||||||
import { imageDimensionsFromStream } from "image-dimensions";
|
import { imageDimensionsFromData } from 'image-dimensions';
|
||||||
import { MediaUploadOptions, UploadFn } from "../media-utils";
|
import { MediaUploadOptions, UploadFn } from '../media-utils';
|
||||||
import { IAttachment } from "../types";
|
import { IAttachment } from '../types';
|
||||||
import { generateNodeId } from "../utils";
|
import { generateNodeId } from '../utils';
|
||||||
import { Node } from "@tiptap/pm/model";
|
import { Node } from '@tiptap/pm/model';
|
||||||
import { Command } from "@tiptap/core";
|
import { Command } from '@tiptap/core';
|
||||||
|
|
||||||
const findImageNodeByPlaceholderId = (
|
const findImageNodeByPlaceholderId = (
|
||||||
doc: Node,
|
doc: Node,
|
||||||
@@ -14,7 +14,7 @@ const findImageNodeByPlaceholderId = (
|
|||||||
doc.descendants((node, pos) => {
|
doc.descendants((node, pos) => {
|
||||||
if (result) return false;
|
if (result) return false;
|
||||||
if (
|
if (
|
||||||
node.type.name === "image" &&
|
node.type.name === 'image' &&
|
||||||
node.attrs.placeholder?.id === placeholderId
|
node.attrs.placeholder?.id === placeholderId
|
||||||
) {
|
) {
|
||||||
result = { node, pos };
|
result = { node, pos };
|
||||||
@@ -34,7 +34,11 @@ const handleImageUpload =
|
|||||||
if (!validated) return;
|
if (!validated) return;
|
||||||
|
|
||||||
const objectUrl = URL.createObjectURL(file);
|
const objectUrl = URL.createObjectURL(file);
|
||||||
const imageDimensions = await imageDimensionsFromStream(file.stream());
|
|
||||||
|
const imageDimensions = imageDimensionsFromData(
|
||||||
|
new Uint8Array(await file.arrayBuffer()),
|
||||||
|
);
|
||||||
|
|
||||||
const placeholderId = generateNodeId();
|
const placeholderId = generateNodeId();
|
||||||
const aspectRatio = imageDimensions
|
const aspectRatio = imageDimensions
|
||||||
? imageDimensions.width / imageDimensions.height
|
? imageDimensions.width / imageDimensions.height
|
||||||
|
|||||||
Reference in New Issue
Block a user