Files
AFFiNE-Mirror/packages/backend/server/src/base/storage/utils.ts
T
DarkSky 8ebdb7452f 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 -->
2026-07-01 22:24:10 +08:00

71 lines
2.0 KiB
TypeScript

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`;