import { runCommand } from "./process"; import { readdir } from "fs/promises"; import { join } from "path"; /** * Extract a .rm file (which is a zip archive) to a temporary directory. * Returns the path to the extraction directory. */ export async function extractRmFile(rmPath: string, outputDir: string): Promise { // .rm files are zip archives const { code, stderr } = await runCommand("unzip", ["-o", rmPath, "-d", outputDir]); if (code !== 0) { throw new Error(`Failed to extract .rm file: ${stderr}`); } return outputDir; } /** * List the contents of the extracted .rm archive. */ export async function listRmContents(extractedDir: string): Promise { const files: string[] = []; async function walk(dir: string): Promise { const entries = await readdir(dir, { withFileTypes: true }); for (const entry of entries) { const entryPath = join(dir, entry.name); if (entry.isDirectory()) { await walk(entryPath); } else if (entry.isFile()) { files.push(entryPath); } } } await walk(extractedDir); return files; } /** * Check if a document is a notebook (handwritten) or a PDF/ebook. * On reMarkable v3.x, notebooks have .rm page files and no content.pdf. * Falls back to rendering a test page if ambiguous. */ export async function isNotebook(extractedDir: string): Promise { const contents = await listRmContents(extractedDir); const hasRmPages = contents.some((f) => f.endsWith(".rm") && !f.endsWith(".zip")); const hasContentPdf = contents.some((f) => f.endsWith("content.pdf") || f.endsWith(".pdf")); // PDF-with-annotations documents have both; treat them as non-notebooks since the PDF is the source return hasRmPages && !hasContentPdf; } /** * Get the list of page .rm files from an extracted notebook. * Returns basenames only. * Recursively searches for .rm files (not just root). */ export async function getPageFiles(extractedDir: string): Promise { const contents = await listRmContents(extractedDir); return contents.filter((f) => f.endsWith(".rm") && !f.endsWith(".zip")).sort(); }