107 lines · 3.7 KB · TypeScript
RawDownload
export const COMPOSER_ATTACHMENT_DIRECTORY = "t3-composer-attachments"; const UUID_PATTERN = "[a-f\\d]{8}-[a-f\\d]{4}-[a-f\\d]{4}-[a-f\\d]{4}-[a-f\\d]{12}";const GENERATED_FILE_NAME = new RegExp(`^${UUID_PATTERN}-`, "i");const IOS_DOCUMENTS_PATH = new RegExp(  `^(.*/Containers/Data/Application/)${UUID_PATTERN}/Documents$`,  "i",);const retainedFiles = new Map<string, number>(); function fileUriPath(uri: string): string | null {  try {    const url = new URL(uri);    if (url.protocol !== "file:" || url.hostname || url.search || url.hash) {      return null;    }    const path = decodeURIComponent(url.pathname);    if (path.includes("\\") || path.includes("\0") || path.split("/").includes("..")) {      return null;    }    return path.startsWith("/private/var/") ? path.slice("/private".length) : path;  } catch {    return null;  }} function ownedFileLocation(uri: string) {  const path = fileUriPath(uri);  if (path === null) {    return null;  }  const separator = `/${COMPOSER_ATTACHMENT_DIRECTORY}/`;  const index = path.lastIndexOf(separator);  const name = index < 0 ? "" : path.slice(index + separator.length);  if (!name || name === "." || name.includes("/")) {    return null;  }  return { documentPath: path.slice(0, index), name };} /** Compares references across iOS data-container moves without rewriting saved drafts. */export function composerAttachmentFileReferenceKey(uri: string): string {  const location = ownedFileLocation(uri);  if (!location) {    return uri;  }  const containerPrefix = GENERATED_FILE_NAME.test(location.name)    ? IOS_DOCUMENTS_PATH.exec(location.documentPath)?.[1]    : undefined;  const documentPath = containerPrefix    ? `${containerPrefix}<app>/Documents`    : location.documentPath;  return `file://${documentPath}/${COMPOSER_ATTACHMENT_DIRECTORY}/${encodeURIComponent(location.name)}`;} /** Holds a local copy until its last player or share-copy operation releases it. */export function retainComposerAttachmentFile(uri: string, onLastRelease: () => void): () => void {  const key = composerAttachmentFileReferenceKey(uri);  retainedFiles.set(key, (retainedFiles.get(key) ?? 0) + 1);  let released = false;  return () => {    if (released) {      return;    }    released = true;    const remaining = (retainedFiles.get(key) ?? 1) - 1;    if (remaining > 0) {      retainedFiles.set(key, remaining);      return;    }    retainedFiles.delete(key);    onLastRelease();  };} export function isComposerAttachmentFileRetained(uri: string): boolean {  return retainedFiles.has(composerAttachmentFileReferenceKey(uri));} /** * Resolves only our saved attachment copies. iOS preserves Documents on updates * but can change its container UUID. Picker and open-in-place source URIs must * bypass this resolver so another app's document keeps its original location. */export function resolveOwnedComposerAttachmentFileUri(  uri: string,  documentDirectoryUri: string,): string | null {  const location = ownedFileLocation(uri);  const documentPath = fileUriPath(documentDirectoryUri)?.replace(/\/+$/, "");  if (!location || !documentPath) {    return null;  }  if (location.documentPath !== documentPath) {    const currentContainerPrefix = IOS_DOCUMENTS_PATH.exec(documentPath)?.[1];    if (      !currentContainerPrefix ||      currentContainerPrefix !== IOS_DOCUMENTS_PATH.exec(location.documentPath)?.[1] ||      !GENERATED_FILE_NAME.test(location.name)    ) {      return null;    }  }  const resolved = new URL(documentDirectoryUri);  resolved.pathname = `${resolved.pathname.replace(/\/+$/, "")}/${COMPOSER_ATTACHMENT_DIRECTORY}/${encodeURIComponent(location.name)}`;  return resolved.href;}