mirror of
https://github.com/toeverything/AFFiNE.git
synced 2026-07-18 18:46:19 +08:00
feat(server): impl storage runtime (#15181)
#### PR Dependency Tree * **PR #15181** 👈 This tree was auto-generated by [Charcoal](https://github.com/danerwilliams/charcoal) <!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit * **New Features** * Added an additional storage backend option: asset-pack based storage (provider for avatar, blob, and copilot). * Introduced a dedicated storage runtime with provider capability reporting and expanded object operations (put/head/get/list/delete), including presigned and multipart flows where supported. * Cloudflare R2 `jurisdiction` now uses an explicit default when omitted. * **Bug Fixes** * Broadened avatar access to allow both fs and asset-pack providers. * Improved workspace blob upload completion validation and handling when stored objects are missing or mismatched. <!-- end of auto-generated comment: release notes by coderabbit.ai -->
This commit is contained in:
@@ -0,0 +1,70 @@
|
||||
import { Readable } from 'node:stream';
|
||||
|
||||
import type { Response } from 'express';
|
||||
import { getStreamAsBuffer } from 'get-stream';
|
||||
|
||||
import { getMime } from '../../native';
|
||||
import type { BlobInputType } from './types';
|
||||
|
||||
export async function toBuffer(input: BlobInputType): Promise<Buffer> {
|
||||
return input instanceof Readable
|
||||
? await getStreamAsBuffer(input)
|
||||
: input instanceof Buffer
|
||||
? input
|
||||
: Buffer.from(input as string);
|
||||
}
|
||||
|
||||
const DANGEROUS_INLINE_MIME_PREFIXES = [
|
||||
'text/html',
|
||||
'application/xhtml+xml',
|
||||
'image/svg+xml',
|
||||
'application/xml',
|
||||
'text/xml',
|
||||
'text/javascript',
|
||||
];
|
||||
|
||||
export function isDangerousInlineMime(mime: string | undefined) {
|
||||
if (!mime) return false;
|
||||
const lower = mime.toLowerCase();
|
||||
return DANGEROUS_INLINE_MIME_PREFIXES.some(p => lower.startsWith(p));
|
||||
}
|
||||
|
||||
export function applyAttachHeaders(
|
||||
res: Response,
|
||||
options: { filename?: string; buffer?: Buffer; contentType?: string }
|
||||
) {
|
||||
let { filename, buffer, contentType } = options;
|
||||
res.setHeader('X-Content-Type-Options', 'nosniff');
|
||||
if (!contentType && buffer) contentType = sniffMime(buffer);
|
||||
if (contentType && isDangerousInlineMime(contentType)) {
|
||||
const safeName = (filename || 'download')
|
||||
.replace(/[\r\n]/g, '')
|
||||
.replace(/[^\w\s.-]/g, '_');
|
||||
res.setHeader(
|
||||
'Content-Disposition',
|
||||
`attachment; filename="${encodeURIComponent(safeName)}"; filename*=UTF-8''${encodeURIComponent(
|
||||
safeName
|
||||
)}`
|
||||
);
|
||||
}
|
||||
if (!res.getHeader('Content-Type')) {
|
||||
res.setHeader('Content-Type', contentType || 'application/octet-stream');
|
||||
}
|
||||
}
|
||||
|
||||
export function sniffMime(
|
||||
buffer: Buffer,
|
||||
declared?: string
|
||||
): string | undefined {
|
||||
try {
|
||||
const detected = getMime(buffer);
|
||||
if (detected) return detected;
|
||||
} catch {}
|
||||
return declared;
|
||||
}
|
||||
|
||||
export const SIGNED_URL_EXPIRED = 60 * 60; // 1 hour
|
||||
|
||||
export const STORAGE_PROXY_ROOT = '/api/storage';
|
||||
export const PROXY_UPLOAD_PATH = `${STORAGE_PROXY_ROOT}/upload`;
|
||||
export const PROXY_MULTIPART_PATH = `${STORAGE_PROXY_ROOT}/multipart`;
|
||||
Reference in New Issue
Block a user