110 lines · 3.5 KB · TypeScript
RawDownload
// @effect-diagnostics nodeBuiltinImport:offimport * as NodeCrypto from "node:crypto";import * as NodeFS from "node:fs"; import type { ChatAttachment } from "@t3tools/contracts"; import {  normalizeAttachmentRelativePath,  resolveAttachmentRelativePath,} from "./attachmentPaths.ts";import { inferImageExtension, SAFE_IMAGE_FILE_EXTENSIONS } from "./imageMime.ts"; const ATTACHMENT_FILENAME_EXTENSIONS = [...SAFE_IMAGE_FILE_EXTENSIONS, ".bin"];const ATTACHMENT_ID_THREAD_SEGMENT_MAX_CHARS = 80;const ATTACHMENT_ID_THREAD_SEGMENT_PATTERN = "[a-z0-9_]+(?:-[a-z0-9_]+)*";const ATTACHMENT_ID_UUID_PATTERN = "[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}";const ATTACHMENT_ID_PATTERN = new RegExp(  `^(${ATTACHMENT_ID_THREAD_SEGMENT_PATTERN})-(${ATTACHMENT_ID_UUID_PATTERN})$`,  "i",); export function toSafeThreadAttachmentSegment(threadId: string): string | null {  const segment = threadId    .trim()    .toLowerCase()    .replace(/[^a-z0-9_-]+/gi, "-")    .replace(/-+/g, "-")    .replace(/^[-_]+|[-_]+$/g, "")    .slice(0, ATTACHMENT_ID_THREAD_SEGMENT_MAX_CHARS)    .replace(/[-_]+$/g, "");  if (segment.length === 0) {    return null;  }  return segment;} export function createAttachmentId(threadId: string): string | null {  const threadSegment = toSafeThreadAttachmentSegment(threadId);  if (!threadSegment) {    return null;  }  return `${threadSegment}-${NodeCrypto.randomUUID()}`;} export function parseThreadSegmentFromAttachmentId(attachmentId: string): string | null {  const normalizedId = normalizeAttachmentRelativePath(attachmentId);  if (!normalizedId || normalizedId.includes("/") || normalizedId.includes(".")) {    return null;  }  const match = normalizedId.match(ATTACHMENT_ID_PATTERN);  if (!match) {    return null;  }  return match[1]?.toLowerCase() ?? null;} export function attachmentRelativePath(attachment: ChatAttachment): string {  switch (attachment.type) {    case "image": {      const extension = inferImageExtension({        mimeType: attachment.mimeType,        fileName: attachment.name,      });      return `${attachment.id}${extension}`;    }  }} export function resolveAttachmentPath(input: {  readonly attachmentsDir: string;  readonly attachment: ChatAttachment;}): string | null {  return resolveAttachmentRelativePath({    attachmentsDir: input.attachmentsDir,    relativePath: attachmentRelativePath(input.attachment),  });} export function resolveAttachmentPathById(input: {  readonly attachmentsDir: string;  readonly attachmentId: string;}): string | null {  const normalizedId = normalizeAttachmentRelativePath(input.attachmentId);  if (!normalizedId || normalizedId.includes("/") || normalizedId.includes(".")) {    return null;  }  for (const extension of ATTACHMENT_FILENAME_EXTENSIONS) {    const maybePath = resolveAttachmentRelativePath({      attachmentsDir: input.attachmentsDir,      relativePath: `${normalizedId}${extension}`,    });    if (maybePath && NodeFS.existsSync(maybePath)) {      return maybePath;    }  }  return null;} export function parseAttachmentIdFromRelativePath(relativePath: string): string | null {  const normalized = normalizeAttachmentRelativePath(relativePath);  if (!normalized || normalized.includes("/")) {    return null;  }  const extensionIndex = normalized.lastIndexOf(".");  if (extensionIndex <= 0) {    return null;  }  const id = normalized.slice(0, extensionIndex);  return id.length > 0 && !id.includes(".") ? id : null;}