fix: msteams attachments + plugin prompt hints
Co-authored-by: Christof <10854026+Evizero@users.noreply.github.com>
This commit is contained in:
@@ -11,6 +11,43 @@ const resolveMediaDir = () => path.join(resolveConfigDir(), "media");
|
||||
const MAX_BYTES = 5 * 1024 * 1024; // 5MB default
|
||||
const DEFAULT_TTL_MS = 2 * 60 * 1000; // 2 minutes
|
||||
|
||||
/**
|
||||
* Sanitize a filename for cross-platform safety.
|
||||
* Removes chars unsafe on Windows/SharePoint/all platforms.
|
||||
* Keeps: alphanumeric, dots, hyphens, underscores, Unicode letters/numbers.
|
||||
*/
|
||||
function sanitizeFilename(name: string): string {
|
||||
// Remove: < > : " / \ | ? * and control chars (U+0000-U+001F)
|
||||
// oxlint-disable-next-line no-control-regex -- Intentionally matching control chars
|
||||
const unsafe = /[<>:"/\\|?*\x00-\x1f]/g;
|
||||
const sanitized = name.trim().replace(unsafe, "_").replace(/\s+/g, "_"); // Replace whitespace runs with underscore
|
||||
// Collapse multiple underscores, trim leading/trailing, limit length
|
||||
return sanitized.replace(/_+/g, "_").replace(/^_|_$/g, "").slice(0, 60);
|
||||
}
|
||||
|
||||
/**
|
||||
* Extract original filename from path if it matches the embedded format.
|
||||
* Pattern: {original}---{uuid}.{ext} → returns "{original}.{ext}"
|
||||
* Falls back to basename if no pattern match, or "file.bin" if empty.
|
||||
*/
|
||||
export function extractOriginalFilename(filePath: string): string {
|
||||
const basename = path.basename(filePath);
|
||||
if (!basename) return "file.bin"; // Fallback for empty input
|
||||
|
||||
const ext = path.extname(basename);
|
||||
const nameWithoutExt = path.basename(basename, ext);
|
||||
|
||||
// Check for ---{uuid} pattern (36 chars: 8-4-4-4-12 with hyphens)
|
||||
const match = nameWithoutExt.match(
|
||||
/^(.+)---[a-f0-9]{8}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{12}$/i,
|
||||
);
|
||||
if (match?.[1]) {
|
||||
return `${match[1]}${ext}`;
|
||||
}
|
||||
|
||||
return basename; // Fallback: use as-is
|
||||
}
|
||||
|
||||
export function getMediaDir() {
|
||||
return resolveMediaDir();
|
||||
}
|
||||
@@ -152,17 +189,29 @@ export async function saveMediaBuffer(
|
||||
contentType?: string,
|
||||
subdir = "inbound",
|
||||
maxBytes = MAX_BYTES,
|
||||
originalFilename?: string,
|
||||
): Promise<SavedMedia> {
|
||||
if (buffer.byteLength > maxBytes) {
|
||||
throw new Error(`Media exceeds ${(maxBytes / (1024 * 1024)).toFixed(0)}MB limit`);
|
||||
}
|
||||
const dir = path.join(resolveMediaDir(), subdir);
|
||||
await fs.mkdir(dir, { recursive: true });
|
||||
const baseId = crypto.randomUUID();
|
||||
const uuid = crypto.randomUUID();
|
||||
const headerExt = extensionForMime(contentType?.split(";")[0]?.trim() ?? undefined);
|
||||
const mime = await detectMime({ buffer, headerMime: contentType });
|
||||
const ext = headerExt ?? extensionForMime(mime);
|
||||
const id = ext ? `${baseId}${ext}` : baseId;
|
||||
const ext = headerExt ?? extensionForMime(mime) ?? "";
|
||||
|
||||
let id: string;
|
||||
if (originalFilename) {
|
||||
// Embed original name: {sanitized}---{uuid}.ext
|
||||
const base = path.parse(originalFilename).name;
|
||||
const sanitized = sanitizeFilename(base);
|
||||
id = sanitized ? `${sanitized}---${uuid}${ext}` : `${uuid}${ext}`;
|
||||
} else {
|
||||
// Legacy: just UUID
|
||||
id = ext ? `${uuid}${ext}` : uuid;
|
||||
}
|
||||
|
||||
const dest = path.join(dir, id);
|
||||
await fs.writeFile(dest, buffer);
|
||||
return { id, path: dest, size: buffer.byteLength, contentType: mime };
|
||||
|
||||
Reference in New Issue
Block a user