<?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: Constantin Alex</title>
    <description>The latest articles on DEV Community by Constantin Alex (@constantin_alex_05c9c24b7).</description>
    <link>https://dev.to/constantin_alex_05c9c24b7</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%2F4173475%2F4145fc68-593c-4f23-a0f2-1a8027584705.png</url>
      <title>DEV Community: Constantin Alex</title>
      <link>https://dev.to/constantin_alex_05c9c24b7</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/constantin_alex_05c9c24b7"/>
    <language>en</language>
    <item>
      <title>I deleted my Rust helper and moved zip extraction into the browser. Here's what it cost.</title>
      <dc:creator>Constantin Alex</dc:creator>
      <pubDate>Mon, 19 Oct 2026 18:57:20 +0000</pubDate>
      <link>https://dev.to/constantin_alex_05c9c24b7/i-deleted-my-rust-helper-and-moved-zip-extraction-into-the-browser-heres-what-it-cost-51l8</link>
      <guid>https://dev.to/constantin_alex_05c9c24b7/i-deleted-my-rust-helper-and-moved-zip-extraction-into-the-browser-heres-what-it-cost-51l8</guid>
      <description>&lt;p&gt;A couple of weeks ago I wrote about &lt;a href="https://dev.to[first%20post]"&gt;LinkUnzip&lt;/a&gt;, a Chrome extension that pulls files out of a zip on the internet without downloading the zip. It reads the zip's table of contents with an HTTP Range request, then downloads only the bytes of the files you want and writes them straight into a folder.&lt;/p&gt;

&lt;p&gt;The idea worked. The install didn't.&lt;/p&gt;

&lt;p&gt;To write files into a real folder, version 1.0 needed a small helper program on your PC. I wrote it in Rust and the extension talked to it through Chrome's native messaging. It was fast. It was also a Windows .exe from someone nobody has heard of, unsigned, so Windows SmartScreen warned about it. A few people told me straight up that the helper was a dealbreaker. And of the first five people who uninstalled the extension, four were on Linux or ChromeOS, where the helper didn't exist at all. They installed it, found out it couldn't work, and left.&lt;/p&gt;

&lt;p&gt;So for 1.1.0 I moved the whole engine into the browser. Here's how that went, including the parts where the browser is slower.&lt;/p&gt;

&lt;h2&gt;
  
  
  What the browser already has
&lt;/h2&gt;

&lt;p&gt;It turns out almost everything the Rust helper did has a browser API now:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;fetch&lt;/code&gt; with a &lt;code&gt;Range&lt;/code&gt; header gets any slice of the zip.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;DecompressionStream("deflate-raw")&lt;/code&gt; inflates zip data natively. Zip uses raw deflate, without the zlib header, and Chrome supports exactly that.&lt;/li&gt;
&lt;li&gt;CRC-32 is about 40 lines of JavaScript with a lookup table.&lt;/li&gt;
&lt;li&gt;The File System Access API (&lt;code&gt;showDirectoryPicker&lt;/code&gt;, &lt;code&gt;createWritable&lt;/code&gt;) writes into a real folder the user picked.&lt;/li&gt;
&lt;li&gt;An offscreen document gives the engine a page that keeps running after the popup closes.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The core is pretty small. Simplified, one file looks like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// fetch one file's compressed bytes and inflate them as they arrive&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;res&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;fetch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;zipUrl&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;headers&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;Range&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;`bytes=&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;start&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;-&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;end&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&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="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;status&lt;/span&gt; &lt;span class="o"&gt;!==&lt;/span&gt; &lt;span class="mi"&gt;206&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;throw&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Error&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;the server ignored the Range header&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;plain&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;body&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;pipeThrough&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;DecompressionStream&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;deflate-raw&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;plain&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;pipeTo&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;fileHandle&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;createWritable&lt;/span&gt;&lt;span class="p"&gt;());&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The real code does more: it groups neighbouring files into one request, skips each file's local header, counts CRC-32 and size on the way through, and only gives the file its real name once both match. But that's the heart of it, and it's all stuff the browser ships.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where the browser is slower
&lt;/h2&gt;

&lt;p&gt;I measured both versions on the same machine.&lt;/p&gt;

&lt;p&gt;The Blender 4.2 zip is 365.8 MB with 5,518 files. The Rust helper extracted all of it in 15 to 17 seconds. The browser version takes 52 seconds.&lt;/p&gt;

&lt;p&gt;The download isn't the problem. The folder is. Chrome runs file system operations one at a time, and each file needs about four of them: get a handle, create a writable, write, close. At roughly 2 ms each, that's about 7 ms per file no matter how many connections are downloading in parallel. For a zip of big files you don't notice. For a zip of thousands of tiny files you do. In a benchmark with 70,000 tiny files, Chrome took 500 seconds where plain Node took 101.&lt;/p&gt;

&lt;p&gt;I decided that's fine as long as the extension says so. The helper still exists as an optional extra for people who want the speed.&lt;/p&gt;

&lt;h2&gt;
  
  
  Chrome has opinions about where you save
&lt;/h2&gt;

&lt;p&gt;My first test build asked you to pick a folder. Then I tried to pick Downloads and Chrome said no. The folder picker refuses Downloads, Desktop and Documents themselves. You can pick a folder inside them, but not the folder itself. For most people "put it in Downloads" is the whole point, so that was a problem.&lt;/p&gt;

&lt;p&gt;The fix was to use Chrome's own downloads for the default case. Each checked file becomes a Blob and goes through &lt;code&gt;chrome.downloads.download&lt;/code&gt; into &lt;code&gt;Downloads/&amp;lt;zip name&amp;gt;/&lt;/code&gt;. No picker, no permission prompt. Three things I learned the hard way:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;If the Blob has no type, Chrome renames every file to .txt. &lt;code&gt;application/octet-stream&lt;/code&gt; fixes it.&lt;/li&gt;
&lt;li&gt;Chrome holds back some file types (.dll, .ini, .cfg, .manifest and a few more) until you click Keep on each one. The extension now counts them before it starts and warns you.&lt;/li&gt;
&lt;li&gt;It's slow: about 150 ms per file, so 6 or 7 files a second. For a zip with thousands of files the popup now tells you how long it will take and suggests picking a folder instead, which is the fast path (that 7 ms per file).&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  The bug that only showed up for real users
&lt;/h2&gt;

&lt;p&gt;The first build worked in all my tests. Then I tried it as a normal user and got "the engine didn't start".&lt;/p&gt;

&lt;p&gt;Chrome stops an extension's background service worker when it's idle. My offscreen page, the one doing the actual work, outlived the worker and was left holding a dead connection. When the worker woke up again, it saw that the page already existed and waited for it to say hello. It never did.&lt;/p&gt;

&lt;p&gt;Now a busy page reconnects by itself, an idle leftover page gets a second and a half and is then replaced, and running extractions survive a worker restart. The test suite stops the worker on purpose, both while idle and in the middle of an extraction, so this can't quietly come back.&lt;/p&gt;

&lt;h2&gt;
  
  
  A bonus: preview
&lt;/h2&gt;

&lt;p&gt;Once the engine could pull one file out of a zip, showing that file was a small step. Every file in the list now has an eye icon that opens it in a tab: text and code, CSV as a table, images, PDF, audio and video. Only that one file is downloaded.&lt;/p&gt;

&lt;p&gt;One photo out of the COCO train2017 dataset, an 18.01 GB zip, downloads 219 KB for the preview. For CSV it stops even earlier: the first 1,000 rows of movies.csv from the MovieLens 32M dataset cost 23.3 KB out of a 227.9 MB zip. Checking whether a dataset is the one you want before downloading 18 GB of it is honestly the feature I use most now.&lt;/p&gt;

&lt;h2&gt;
  
  
  What still doesn't work
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Servers that ignore Range requests and send the whole file every time. GitHub's green "Download ZIP" button is one of them. For those you still need the helper, which streams the file once from start to end.&lt;/li&gt;
&lt;li&gt;RAR, 7z and password-protected zips.&lt;/li&gt;
&lt;li&gt;I've only tested it on Windows so far. It should work in Chrome on Mac, Linux and ChromeOS, since nothing in it is Windows-specific any more, but I haven't seen it with my own eyes yet.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Try it
&lt;/h2&gt;

&lt;p&gt;It's free and open source, no account, nothing to install besides the extension:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Chrome Web Store: &lt;a href="https://chromewebstore.google.com/detail/dapeiljhjlaelolngnoagobeojcpkica?utm_source=devto&amp;amp;utm_medium=article" rel="noopener noreferrer"&gt;https://chromewebstore.google.com/detail/dapeiljhjlaelolngnoagobeojcpkica?utm_source=devto&amp;amp;utm_medium=article&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Code: &lt;a href="https://github.com/CincaAlex/LinkUnzip" rel="noopener noreferrer"&gt;https://github.com/CincaAlex/LinkUnzip&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If you're on Mac or Linux I'd really like to know if it works for you. And if you've hit the same Chrome limits with the File System Access API, how did you get around the one-operation-at-a-time thing? I couldn't find a way.&lt;/p&gt;

</description>
      <category>showdev</category>
      <category>javascript</category>
      <category>webdev</category>
      <category>opensource</category>
    </item>
    <item>
      <title>I didn't have room for a zip and its files, so I learned how to skip the zip</title>
      <dc:creator>Constantin Alex</dc:creator>
      <pubDate>Fri, 09 Oct 2026 12:59:33 +0000</pubDate>
      <link>https://dev.to/constantin_alex_05c9c24b7/i-didnt-have-room-for-a-zip-and-its-files-so-i-learned-how-to-skip-the-zip-3gg6</link>
      <guid>https://dev.to/constantin_alex_05c9c24b7/i-didnt-have-room-for-a-zip-and-its-files-so-i-learned-how-to-skip-the-zip-3gg6</guid>
      <description>&lt;p&gt;A while ago I had to unpack a 60 GB zip. The files would have fit on my laptop. The zip plus the files didn't.&lt;/p&gt;

&lt;p&gt;That's the annoying part of "download it, extract it, delete the zip". For a moment you need room for both. Usually that's fine. That day it wasn't, and I got stuck on a question: do I actually need the zip on my disk at all?&lt;/p&gt;

&lt;p&gt;Turns out, not really.&lt;/p&gt;

&lt;h2&gt;
  
  
  The trick
&lt;/h2&gt;

&lt;p&gt;A zip file keeps its table of contents at the very end. It's called the central directory, and it lists every file in the archive, how big it is and where exactly it starts inside the zip.&lt;/p&gt;

&lt;p&gt;The second piece is something most servers already support: HTTP Range requests. You can ask a server for just a slice of a file, like "give me bytes 1000 to 2000". It's the same thing that lets you skip ahead in a video or resume a download.&lt;/p&gt;

&lt;p&gt;Put those together and you get this:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Ask the server how big the zip is.&lt;/li&gt;
&lt;li&gt;Download only the end of it and read the table of contents.&lt;/li&gt;
&lt;li&gt;Now you know every file and where it lives, so download just the bytes of the files you want.&lt;/li&gt;
&lt;li&gt;Unpack each one as it arrives and write it straight into a folder.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The zip itself never gets saved anywhere.&lt;/p&gt;

&lt;h2&gt;
  
  
  The numbers
&lt;/h2&gt;

&lt;p&gt;This is the part that still feels a bit like cheating to me.&lt;/p&gt;

&lt;p&gt;COCO train2017 is a popular machine learning dataset: an 18.01 GB zip with 118,287 photos. Getting one photo out of it downloaded 12.2 MB in total, and 11.9 MB of that was just the table of contents. It took between 3.7 and 6.8 seconds over four runs.&lt;/p&gt;

&lt;p&gt;For a full archive: the Blender 4.2 zip is 365.8 MB with 5,518 files. Extracting all of it wrote 925.6 MB of files with 0 B of zip on disk, in 15 to 22 seconds on my home connection. The normal way, you'd need 1.26 GB free for the zip and the files together.&lt;/p&gt;

&lt;p&gt;To be clear, for a whole archive it downloads about the same amount as a normal download. It's not faster magic. You save the space the zip would take, and when you only need part of an archive, you only download that part.&lt;/p&gt;

&lt;h2&gt;
  
  
  From a script to an extension
&lt;/h2&gt;

&lt;p&gt;It started as a small command line tool in Rust, just for me. You gave it a link and a folder, and it did the rest.&lt;/p&gt;

&lt;p&gt;Then I got tired of copying links into a terminal and wanted it on a right-click. So it became a Chrome extension. The catch: an extension can't really write a whole folder tree to your disk on its own. So there's a small Windows helper program, and the extension talks to it through Chrome's native messaging. The extension shows the list and the buttons, the helper does the downloading and writing.&lt;/p&gt;

&lt;p&gt;Now it's: right-click a zip link, see what's inside and how much space it needs, pick the files, done.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Frgmooajfed0klsteuurj.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Frgmooajfed0klsteuurj.png" alt=" " width="800" height="500"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Things that broke along the way
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;GitHub doesn't play along.&lt;/strong&gt; The "Download ZIP" button on GitHub doesn't support Range requests at all, so the whole trick falls apart there. I ended up writing a second mode that reads the zip once from start to finish and unpacks files as they go by. No preview, but still no zip on disk.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Zips can be nasty.&lt;/strong&gt; A zip can contain a file called CON or NUL.txt, which Windows simply refuses to create. Worse, it can contain paths like ../../something that try to write outside the folder you picked. All of that has to be checked before a single byte gets written.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Every file gets checked.&lt;/strong&gt; Each file is written under a temporary name first, its checksum and size are compared with what the zip says, and only then does it get its real name. If the connection drops halfway, you don't end up with a half-written file that looks fine.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The Chrome Web Store review.&lt;/strong&gt; An extension that talks to a native program and can ask for cookies (for downloads that need your login) gets a careful review. Explaining every permission clearly took a while, but it went public this week.&lt;/p&gt;

&lt;h2&gt;
  
  
  The honest limits
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Windows only for now&lt;/li&gt;
&lt;li&gt;It needs the helper installed (per user, no admin rights)&lt;/li&gt;
&lt;li&gt;No RAR or 7z yet, and no password-protected zips&lt;/li&gt;
&lt;li&gt;The installer isn't code-signed yet, so Windows SmartScreen shows a warning the first time&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Try it or read the code
&lt;/h2&gt;

&lt;p&gt;It's free and open source, no account and no tracking. It's called LinkUnzip.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Code: &lt;a href="https://github.com/CincaAlex/LinkUnzip" rel="noopener noreferrer"&gt;https://github.com/CincaAlex/LinkUnzip&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Website: &lt;a href="https://linkunzip.app" rel="noopener noreferrer"&gt;https://linkunzip.app&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;I'm curious whether other people run into this too, or if it's just me. And if you know a zip link it chokes on, I'd really like to hear about it.&lt;/p&gt;

</description>
      <category>rust</category>
      <category>showdev</category>
      <category>javascript</category>
      <category>webdev</category>
    </item>
  </channel>
</rss>
