<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: jjopensoftworks-blip</title>
    <description>The latest articles on DEV Community by jjopensoftworks-blip (@jjopensoftworksblip).</description>
    <link>https://dev.to/jjopensoftworksblip</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F4035596%2F0cc9e973-313b-418a-a298-d34d9aaa6422.png</url>
      <title>DEV Community: jjopensoftworks-blip</title>
      <link>https://dev.to/jjopensoftworksblip</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/jjopensoftworksblip"/>
    <language>en</language>
    <item>
      <title>Scrubkit: point it at a folder, get clean text + metadata back — 100% offline</title>
      <dc:creator>jjopensoftworks-blip</dc:creator>
      <pubDate>Sun, 19 Jul 2026 08:42:41 +0000</pubDate>
      <link>https://dev.to/jjopensoftworksblip/scrubkit-point-it-at-a-folder-get-clean-text-metadata-back-100-offline-13ai</link>
      <guid>https://dev.to/jjopensoftworksblip/scrubkit-point-it-at-a-folder-get-clean-text-metadata-back-100-offline-13ai</guid>
      <description>&lt;p&gt;If you've ever built RAG ingestion, a search index, or any "read a pile of documents" feature&lt;br&gt;
in .NET, you know the annoying part isn't the embeddings or the vector store. It's step zero:&lt;br&gt;
&lt;strong&gt;getting clean text out of a messy folder of PDFs, Word docs, spreadsheets, emails, and&lt;br&gt;
whatever else landed there&lt;/strong&gt; — without shipping the files off to some cloud API.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Scrubkit&lt;/strong&gt; does exactly that, and nothing leaves your machine.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="k"&gt;using&lt;/span&gt; &lt;span class="nn"&gt;Scrubkit&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;scrubber&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nf"&gt;FolderScrubber&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="n"&gt;ReadOptions&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="n"&gt;Recursion&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;Recursion&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;AllNested&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;

&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="k"&gt;foreach&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;doc&lt;/span&gt; &lt;span class="k"&gt;in&lt;/span&gt; &lt;span class="n"&gt;scrubber&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;ReadStreamAsync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;@"C:\Docs"&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;doc&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Text&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Length&lt;/span&gt; &lt;span class="p"&gt;==&lt;/span&gt; &lt;span class="m"&gt;0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;continue&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;           &lt;span class="c1"&gt;// skip metadata-only rows&lt;/span&gt;
    &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;index&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;UpsertAsync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;doc&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Path&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;doc&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Text&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;doc&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Metadata&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;One call, one flat table: per file you get &lt;code&gt;Text&lt;/code&gt;, &lt;code&gt;Metadata&lt;/code&gt;, &lt;code&gt;TypeBucket&lt;/code&gt;, &lt;code&gt;SizeBytes&lt;/code&gt;,&lt;br&gt;
&lt;code&gt;Modified&lt;/code&gt;, and any &lt;code&gt;Warnings&lt;/code&gt;. No network calls, no telemetry — usable in air-gapped and&lt;br&gt;
regulated environments.&lt;/p&gt;
&lt;h3&gt;
  
  
  What it reads out of the box
&lt;/h3&gt;

&lt;p&gt;PDF (PdfPig), Office &lt;code&gt;docx&lt;/code&gt;/&lt;code&gt;pptx&lt;/code&gt;/&lt;code&gt;xlsx&lt;/code&gt;, the plain-text family (&lt;code&gt;txt&lt;/code&gt;/&lt;code&gt;md&lt;/code&gt;/&lt;code&gt;csv&lt;/code&gt;/&lt;code&gt;json&lt;/code&gt;/&lt;br&gt;
&lt;code&gt;xml&lt;/code&gt;/&lt;code&gt;html&lt;/code&gt;/…), and image EXIF. Unknown types come back as a metadata-only row. A single&lt;br&gt;
unreadable file &lt;strong&gt;never crashes the batch&lt;/strong&gt; — the problem shows up as a &lt;code&gt;Warning&lt;/code&gt; on that row.&lt;/p&gt;
&lt;h3&gt;
  
  
  Built like a real library, not a gist
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Multi-targeted&lt;/strong&gt; &lt;code&gt;net8.0&lt;/code&gt; + &lt;code&gt;netstandard2.0&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;114 tests, ~99% line coverage&lt;/strong&gt;, an 85% gate in CI (Linux + Windows).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Package validation&lt;/strong&gt; guards the public API against accidental breaking changes.&lt;/li&gt;
&lt;li&gt;Deterministic, &lt;strong&gt;SourceLink&lt;/strong&gt;ed builds; tag-driven versioning (MinVer); symbol packages.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Fast:&lt;/strong&gt; a BenchmarkDotNet run extracts on the order of &lt;strong&gt;~12,000 files/sec&lt;/strong&gt; (4-way
parallel) over a mixed text-file corpus.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;
  
  
  Extensible without forking
&lt;/h3&gt;

&lt;p&gt;Adding a format is one interface:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;sealed&lt;/span&gt; &lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;MyExtractor&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;IFileExtractor&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="kt"&gt;bool&lt;/span&gt; &lt;span class="nf"&gt;CanHandle&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="n"&gt;ext&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;ext&lt;/span&gt; &lt;span class="p"&gt;==&lt;/span&gt; &lt;span class="s"&gt;".xyz"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="n"&gt;ExtractedContent&lt;/span&gt; &lt;span class="nf"&gt;Extract&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="n"&gt;path&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;metadata&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;text&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="n"&gt;options&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Extractors&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Add&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nf"&gt;MyExtractor&lt;/span&gt;&lt;span class="p"&gt;());&lt;/span&gt;   &lt;span class="c1"&gt;// tried before the built-ins&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Add-ons reference only the tiny &lt;strong&gt;&lt;code&gt;Scrubkit.Abstractions&lt;/code&gt;&lt;/strong&gt; contracts package — no PDF or&lt;br&gt;
image dependencies pulled in. There's already a growing family:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;Scrubkit.Email&lt;/code&gt;&lt;/strong&gt; — &lt;code&gt;.eml&lt;/code&gt; (MIME): headers → metadata, body → text.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;Scrubkit.OpenDocument&lt;/code&gt;&lt;/strong&gt; — &lt;code&gt;.odt&lt;/code&gt; / &lt;code&gt;.ods&lt;/code&gt; / &lt;code&gt;.odp&lt;/code&gt; from LibreOffice / OpenOffice.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;Scrubkit.Epub&lt;/code&gt;&lt;/strong&gt; — &lt;code&gt;.epub&lt;/code&gt; e-books.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;Scrubkit.Extensions.DependencyInjection&lt;/code&gt;&lt;/strong&gt; — &lt;code&gt;services.AddScrubkit(…)&lt;/code&gt; for ASP.NET Core
and worker hosts.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Redaction, if you want it
&lt;/h3&gt;

&lt;p&gt;Text is returned exactly as read. Opt into scrubbing common PII (emails, phones,&lt;br&gt;
Luhn-checked cards, SSNs, IPs) by supplying an &lt;code&gt;IRedactor&lt;/code&gt; — it's entirely your call, and&lt;br&gt;
it's explicitly best-effort, not a compliance tool.&lt;/p&gt;

&lt;h3&gt;
  
  
  Try it in 30 seconds
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;dotnet add package Scrubkit
&lt;span class="c"&gt;# or, without installing:&lt;/span&gt;
dotnet run &lt;span class="nt"&gt;--project&lt;/span&gt; samples/Scrubkit.Playground
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;NuGet:&lt;/strong&gt; &lt;a href="https://www.nuget.org/packages/Scrubkit" rel="noopener noreferrer"&gt;https://www.nuget.org/packages/Scrubkit&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Site + docs:&lt;/strong&gt; &lt;a href="https://jjopensoftworks-blip.github.io/Scrubkit/" rel="noopener noreferrer"&gt;https://jjopensoftworks-blip.github.io/Scrubkit/&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;GitHub:&lt;/strong&gt; &lt;a href="https://github.com/jjopensoftworks-blip/Scrubkit" rel="noopener noreferrer"&gt;https://github.com/jjopensoftworks-blip/Scrubkit&lt;/a&gt; — a ⭐ helps a lot.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Feedback and format requests welcome. What would you point it at?&lt;/p&gt;

</description>
      <category>dotnet</category>
      <category>csharp</category>
      <category>showdev</category>
      <category>ai</category>
    </item>
  </channel>
</rss>
