mirror of
https://github.com/docmost/docmost.git
synced 2026-09-11 07:56:54 +08:00
expand file type support
This commit is contained in:
@@ -21,7 +21,7 @@ import { InjectQueue } from '@nestjs/bullmq';
|
|||||||
import { Queue } from 'bullmq';
|
import { Queue } from 'bullmq';
|
||||||
import { QueueJob, QueueName } from '../../queue/constants';
|
import { QueueJob, QueueName } from '../../queue/constants';
|
||||||
import { isBase64 } from "class-validator";
|
import { isBase64 } from "class-validator";
|
||||||
import { EnvironmentService } from "src/integrations/environment/environment.service";
|
import { EnvironmentService } from '../../environment/environment.service';
|
||||||
import * as bytes from 'bytes';
|
import * as bytes from 'bytes';
|
||||||
import * as mimeTypes from 'mime-types';
|
import * as mimeTypes from 'mime-types';
|
||||||
import { dbOrTx } from '@docmost/db/utils';
|
import { dbOrTx } from '@docmost/db/utils';
|
||||||
@@ -78,7 +78,6 @@ export class ImportAttachmentService {
|
|||||||
* Archive imports use processAttachments() because their files already
|
* Archive imports use processAttachments() because their files already
|
||||||
* exist on disk; standalone imports need to materialize these payloads.
|
* exist on disk; standalone imports need to materialize these payloads.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
async processEmbeddedAttachments(
|
async processEmbeddedAttachments(
|
||||||
opts: AttachmentMeta & { html: string },
|
opts: AttachmentMeta & { html: string },
|
||||||
): Promise<string> {
|
): Promise<string> {
|
||||||
@@ -86,18 +85,25 @@ export class ImportAttachmentService {
|
|||||||
const $ = load(html);
|
const $ = load(html);
|
||||||
const processed = new Map<string, string>();
|
const processed = new Map<string, string>();
|
||||||
|
|
||||||
|
const resolveUri = async (uri: string): Promise<string | null> => {
|
||||||
|
const normalized = uri?.trim();
|
||||||
|
if (!normalized) return null;
|
||||||
|
if (processed.has(normalized)) return processed.get(normalized);
|
||||||
|
|
||||||
|
const apiFilePath = await this.uploadDataUri({
|
||||||
|
uri: normalized,
|
||||||
|
...rest,
|
||||||
|
});
|
||||||
|
if (apiFilePath) processed.set(normalized, apiFilePath);
|
||||||
|
return apiFilePath;
|
||||||
|
};
|
||||||
|
|
||||||
|
// img/video/audio/source: src attribute
|
||||||
for (const element of $(
|
for (const element of $(
|
||||||
'img[src], video[src], audio[src], source[src]',
|
'img[src], video[src], audio[src], source[src]',
|
||||||
).toArray()) {
|
).toArray()) {
|
||||||
const $element = $(element);
|
const $element = $(element);
|
||||||
const src = $element.attr('src');
|
const apiFilePath = await resolveUri($element.attr('src'));
|
||||||
const normalized = src.trim();
|
|
||||||
|
|
||||||
let apiFilePath = processed.get(normalized);
|
|
||||||
if (!apiFilePath) {
|
|
||||||
apiFilePath = await this.uploadDataUri({ uri: normalized, ...rest });
|
|
||||||
processed.set(normalized, apiFilePath);
|
|
||||||
}
|
|
||||||
if (!apiFilePath) continue;
|
if (!apiFilePath) continue;
|
||||||
|
|
||||||
$element
|
$element
|
||||||
@@ -108,16 +114,134 @@ export class ImportAttachmentService {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// video poster (thumbnail image, often a separate data URI)
|
||||||
|
for (const element of $('video[poster]').toArray()) {
|
||||||
|
const $element = $(element);
|
||||||
|
const apiFilePath = await resolveUri($element.attr('poster'));
|
||||||
|
|
||||||
|
console.log({apiFilePath})
|
||||||
|
if (apiFilePath) {
|
||||||
|
$element
|
||||||
|
.attr('poster', apiFilePath)
|
||||||
|
.attr('src', apiFilePath)
|
||||||
|
.attr('preload', 'metadata')
|
||||||
|
.attr('controls')
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// the client does not currently support srcset.
|
||||||
|
// so we upload only the highest resolution, or the first image
|
||||||
|
for (const element of $('img[srcset]').toArray()) {
|
||||||
|
const $element = $(element);
|
||||||
|
const srcset = $element.attr('srcset');
|
||||||
|
if (!srcset) continue;
|
||||||
|
|
||||||
|
const candidates = srcset.split(/,\s+(?=\S)/);
|
||||||
|
|
||||||
|
let firstEmbeddedCandidate: string | undefined;
|
||||||
|
let bestWidthCandidate: any;
|
||||||
|
let bestDensityCandidate: any;
|
||||||
|
for (const candidate of candidates) {
|
||||||
|
if (!candidate) continue;
|
||||||
|
|
||||||
|
const trimmed = candidate.trim();
|
||||||
|
const match = trimmed.match(/^(\S+)(?:\s+(\d+(?:\.\d+)?)(w|x))?$/i);
|
||||||
|
if (!match) continue;
|
||||||
|
|
||||||
|
const [, uri, descriptorValue, descriptorType] = match;
|
||||||
|
if (!uri.toLowerCase().startsWith('data:')) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
const value = descriptorValue ? Number(descriptorValue) : undefined;
|
||||||
|
const type = descriptorType?.toLowerCase();
|
||||||
|
|
||||||
|
// Keep the first embedded image as the final fallback.
|
||||||
|
if (!firstEmbeddedCandidate) {
|
||||||
|
firstEmbeddedCandidate = uri;
|
||||||
|
if (type !== "w" && type !== "x"){
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (
|
||||||
|
type === 'w' &&
|
||||||
|
value !== undefined &&
|
||||||
|
(!bestWidthCandidate || value > bestWidthCandidate.width)
|
||||||
|
) {
|
||||||
|
bestWidthCandidate = {
|
||||||
|
uri,
|
||||||
|
width: value,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
if (
|
||||||
|
type === 'x' &&
|
||||||
|
value !== undefined &&
|
||||||
|
(!bestDensityCandidate || value > bestDensityCandidate.density)
|
||||||
|
) {
|
||||||
|
bestDensityCandidate = {
|
||||||
|
uri,
|
||||||
|
density: value,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const selectedUri =
|
||||||
|
bestWidthCandidate?.uri ??
|
||||||
|
bestDensityCandidate?.uri ??
|
||||||
|
firstEmbeddedCandidate;
|
||||||
|
|
||||||
|
const apiFilePath = await resolveUri(selectedUri);
|
||||||
|
if (!apiFilePath) continue;
|
||||||
|
|
||||||
|
$element
|
||||||
|
.attr('src', apiFilePath)
|
||||||
|
.attr('data-attachment-id', apiFilePath.split('/')[3])
|
||||||
|
.attr('data-align', $element.attr('data-align') ?? 'center');
|
||||||
|
}
|
||||||
|
|
||||||
|
// the client represents embeds as iframes.
|
||||||
|
// so we convert embeds and objects as iframes
|
||||||
|
for (const element of $('object[data], embed[src]').toArray()) {
|
||||||
|
const $element = $(element);
|
||||||
|
const sourceAttribute = $element.is('object') ? 'data' : 'src';
|
||||||
|
const uri = $element.attr(sourceAttribute);
|
||||||
|
|
||||||
|
const apiFilePath = await resolveUri(uri);
|
||||||
|
if (!apiFilePath) continue;
|
||||||
|
|
||||||
|
const $iframe = $('<iframe>')
|
||||||
|
.attr('src', apiFilePath)
|
||||||
|
.attr('data-attachment-id', apiFilePath.split('/')[3]);
|
||||||
|
|
||||||
|
for (const attribute of ['width', 'height', 'title']) {
|
||||||
|
const value = $element.attr(attribute);
|
||||||
|
if (value) {
|
||||||
|
$iframe.attr(attribute, value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$element.replaceWith($iframe);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Rewrite existing iframe data URIs.
|
||||||
|
for (const element of $('iframe[src]').toArray()) {
|
||||||
|
const $iframe = $(element);
|
||||||
|
const uri = $iframe.attr('src');
|
||||||
|
|
||||||
|
const apiFilePath = await resolveUri(uri);
|
||||||
|
if (!apiFilePath) continue;
|
||||||
|
|
||||||
|
$iframe
|
||||||
|
.attr('src', apiFilePath)
|
||||||
|
.attr('data-attachment-id', apiFilePath.split('/')[3]);
|
||||||
|
}
|
||||||
|
|
||||||
|
// anchors
|
||||||
for (const element of $('a[href]').toArray()) {
|
for (const element of $('a[href]').toArray()) {
|
||||||
const $element = $(element);
|
const $element = $(element);
|
||||||
const href = $element.attr('href');
|
const apiFilePath = await resolveUri($element.attr('href'));
|
||||||
const normalized = href.trim();
|
|
||||||
|
|
||||||
let apiFilePath = processed.get(normalized);
|
|
||||||
if (!apiFilePath) {
|
|
||||||
apiFilePath = await this.uploadDataUri({ uri: normalized, ...rest });
|
|
||||||
processed.set(normalized, apiFilePath);
|
|
||||||
}
|
|
||||||
if (apiFilePath) $element.attr('href', apiFilePath);
|
if (apiFilePath) $element.attr('href', apiFilePath);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -753,7 +877,7 @@ export class ImportAttachmentService {
|
|||||||
workspaceId,
|
workspaceId,
|
||||||
pageId,
|
pageId,
|
||||||
spaceId,
|
spaceId,
|
||||||
trx
|
trx,
|
||||||
}: AttachmentMeta & { uri: string }): Promise<string | null> => {
|
}: AttachmentMeta & { uri: string }): Promise<string | null> => {
|
||||||
if (!uri.toLowerCase().startsWith('data:')) {
|
if (!uri.toLowerCase().startsWith('data:')) {
|
||||||
return null;
|
return null;
|
||||||
@@ -805,7 +929,7 @@ export class ImportAttachmentService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const attachmentId = v7();
|
const attachmentId = v7();
|
||||||
const fileName = `imported-${attachmentId}` + fileExt;
|
const fileName = `${attachmentId}` + fileExt;
|
||||||
const storageFilePath = `${getAttachmentFolderPath(
|
const storageFilePath = `${getAttachmentFolderPath(
|
||||||
AttachmentType.File,
|
AttachmentType.File,
|
||||||
workspaceId,
|
workspaceId,
|
||||||
@@ -813,7 +937,7 @@ export class ImportAttachmentService {
|
|||||||
const apiFilePath = `/api/files/${attachmentId}/${fileName}`;
|
const apiFilePath = `/api/files/${attachmentId}/${fileName}`;
|
||||||
|
|
||||||
await this.storageService.upload(storageFilePath, buffer);
|
await this.storageService.upload(storageFilePath, buffer);
|
||||||
const db = dbOrTx(this.db, trx)
|
const db = dbOrTx(this.db, trx);
|
||||||
await db
|
await db
|
||||||
.insertInto('attachments')
|
.insertInto('attachments')
|
||||||
.values({
|
.values({
|
||||||
|
|||||||
Reference in New Issue
Block a user