// ingestion/internal/extract/extract.go package extract import ( "fmt" "io" "os" "strings" ) // Text reads the file at path and returns its plain-text content. // Supported extensions: .md, .txt (passthrough), .pdf (via pdftotext), // .docx/.xlsx/.pptx/.png/.jpg/.jpeg (via docmark, ADR-0013 -- requires // DOCMARK_URL to be set; see docmark.go). func Text(path string) (string, error) { ext := strings.ToLower(fileExt(path)) switch ext { case ".md", ".txt": b, err := os.ReadFile(path) if err != nil { return "", fmt.Errorf("read %s: %w", path, err) } return string(b), nil case ".pdf": return extractPDF(path) case ".docx", ".xlsx", ".pptx", ".png", ".jpg", ".jpeg": return extractViaDocmark(path) default: return "", fmt.Errorf("unsupported file extension: %s", ext) } } // fileExt returns the file extension including the dot, lowercased. func fileExt(path string) string { for i := len(path) - 1; i >= 0; i-- { if path[i] == '.' { return path[i:] } if path[i] == '/' || path[i] == '\\' { break } } return "" } // TextIn is Text for a file read through an os.Root (hyperguild#153): the // read cannot leave the root, so a path checked earlier cannot be swapped // for a symlink pointing elsewhere before it is read. Formats that need an // external tool (pdf, docmark) are copied to a temp file from the handle // opened through the root, then extracted from the copy. func TextIn(r *os.Root, rel string) (string, error) { fi, err := r.Lstat(rel) if err != nil { return "", fmt.Errorf("stat %s: %w", rel, err) } if !fi.Mode().IsRegular() { return "", fmt.Errorf("%s: not a regular file", rel) } f, err := r.Open(rel) if err != nil { return "", fmt.Errorf("open %s: %w", rel, err) } defer func() { _ = f.Close() }() ext := strings.ToLower(fileExt(rel)) switch ext { case ".md", ".txt": b, err := io.ReadAll(f) if err != nil { return "", fmt.Errorf("read %s: %w", rel, err) } return string(b), nil case ".pdf", ".docx", ".xlsx", ".pptx", ".png", ".jpg", ".jpeg": tmp, err := os.CreateTemp("", "ingest-*"+ext) if err != nil { return "", fmt.Errorf("temp copy: %w", err) } defer func() { _ = os.Remove(tmp.Name()) }() if _, err := io.Copy(tmp, f); err != nil { _ = tmp.Close() return "", fmt.Errorf("copy %s: %w", rel, err) } if err := tmp.Close(); err != nil { return "", fmt.Errorf("copy %s: %w", rel, err) } return Text(tmp.Name()) default: return "", fmt.Errorf("unsupported file extension: %s", ext) } }