← All AI Scripts Backup · bash

backuptousb

Packs folders straight onto an external disk as one compressed archive per folder. Written because copying many small files to USB is slow in a way that makes no sense until you know why.

Free to use Linux + bash tar + zstd No installation

The problem it solves

Drag a folder of 50,000 small files onto a USB stick and you get 2 MB/s on a disk that can comfortably do 100. The disk is not the slow part. Every single file means a round-trip of open, write, close, and those round-trips are what you are waiting for, not the data itself.

One archive is a single sequential stream of bytes. That gets you full disk speed. This is the whole trick, and tar has been able to do it for forty years. This script just wraps it in something you can stand to use daily, with progress, safety rails and sensible defaults.

Getting started

Download the file, make it executable, and add an alias so it is available anywhere:

bash
$ chmod +x ~/scripts/backuptousb
$ echo "alias backuptousb='~/scripts/backuptousb'" >> ~/.bashrc
$ source ~/.bashrc

Then open the file and set the default destination at the top, in the EDIT ME block. It is the only setting most people need to touch:

backuptousb, EDIT ME
DEFAULT_DEST="/media/youruser/usbdiskname/backups"   # where archives land
COMPRESSION="zstd"                        # zstd | gzip | none
ZSTD_LEVEL="3"                           # 1-19; 3 is fast and good
DATESTAMP="no"                            # yes -> claude-20260716.tar.zst

Usage

Run it from the folder above whatever you are packing. The script insists on relative paths for exactly this reason: the archive then contains myproject/…, and you can extract it anywhere without dragging /home/you/ into the archive.

backuptousb folder/Packs one folder. Press Enter to accept the suggested filename.
backuptousb a/ b/ c/One archive per folder. You are asked for the destination once, then it works through the list unattended.
backuptousb notes.txtSingle files work just as well as folders.
-yNo prompts. Uses the default folder. For cron and scripts.
-o <path>Overrides the name and location for a single archive.
-hPrints the whole comment block from the top of the file.

A typical run

Here a project folder and a loose file are packed in one go. Note that node_modules is never counted: the folder held 53 files, the script packs 55 entries (files plus directories) and skips the rest:

backuptousb myproject/ notes.txt
$ cd ~
$ backuptousb myproject/ notes.txt

2 sources to back up:
  myproject/  ->  myproject.tar.zst
  notes.txt  ->  notes.txt.tar.zst

Destination folder (Enter to accept): /media/youruser/usbdiskname/backups

[1/2] myproject/

Packing  myproject/
     ->  /media/youruser/usbdiskname/backups/myproject.tar.zst

Sizing... 55 files, 220K

[#][#][#][#][#][#][#][#][#][#][#][#][#][#][#][#][#][#][#][#][#][#][#][#][#][#][#][#] 100%  00:00 taken

Flushing to disk... done

  Source    220K  (55 files)
  Archive   689  ->  /media/youruser/usbdiskname/backups/myproject.tar.zst
  Shrunk to 0.3% of original
  Time      0m 0s
  Speed     (too fast to measure)

[2/2] notes.txt

Packing  notes.txt
     ->  /media/youruser/usbdiskname/backups/notes.txt.tar.zst

Sizing... 1 files, 4.0K

[#][#][#][#][#][#][#][#][#][#][#][#][#][#][#][#][#][#][#][#][#][#][#][#][#][#][#][#] 100%  00:00 taken

Flushing to disk... done

  Source    4.0K  (1 files)
  Archive   102  ->  /media/youruser/usbdiskname/backups/notes.txt.tar.zst
  Shrunk to 2.4% of original
  Time      0m 0s
  Speed     (too fast to measure)

========================================
  2 of 2 sources packed
  224K  ->  791  in  0m 0s
========================================

Scripts on the backup disk:
  + /media/youruser/usbdiskname/backups/backuptousb
  + /media/youruser/usbdiskname/backups/extracttodisk

To restore:  /media/youruser/usbdiskname/backups/extracttodisk <archive.tar.zst>

On real data the numbers look different, of course. A 60 GB folder takes minutes, not seconds, and then it is the ETA in the progress bar you watch. The point of the output is that you get to see what actually happened: how much was read, how big the archive got, and where it is.

When the archive already exists

Run it again over an archive that already exists and you get the numbers for both before deciding. Not just "file exists, overwrite?", but how old the existing archive is and how big the source is now:

overwrite
myproject.tar.zst already exists:
  existing archive       689   packed 2026-08-05 12:15
  myproject/ source now  220K   (55 files, uncompressed)

Overwrite? [y/N] _

The safety rails

An interrupted run does not destroy what you had

tar normally writes straight into the target file. Kill the run halfway and you are left with a truncated file under the correct name: a backup that looks like a backup but is not one. This script always packs to archive.tar.zst.partial first and renames it only after tar exits cleanly. Ctrl-C, a power cut or a full disk means the half file is cleaned up and the previous, complete archive is left untouched.

One unreadable file does not stop a 60 GB backup

This is not theory. A root-owned file in the middle of the tree aborted a 60 GB run at 55 GB. Now tar runs with --ignore-failed-read: unreadable files are skipped, counted on screen, and listed in a .skipped.log next to the archive. The run completes, and you find out exactly what was missing.

Progress that is actually correct

The progress bar counts files, not bytes. Byte-based progress goes badly wrong on trees of many small files, because each file is rounded up to a 512-byte tar block. Measured on a tree of 20,000 tiny files, a byte-based bar jumped from 38% straight to 100%. Counting files is exact at any file size.

Database directories are skipped on purpose. db-data and mysql-data are in the exclude list. A live InnoDB directory copied out from under a running server is a torn snapshot that may well not restore at all. Take a mariadb-dump into the project folder instead, and the dump gets picked up as an ordinary file.

What is not included

There is no incremental logic. Every run re-reads the whole source. If you want "only what changed", rsync is the right tool alongside this, not tar.

The script writes straight to USB. If the cable goes mid-write you have a truncated file and no local copy. For anything genuinely critical: pack to local disk first, then copy the single finished file across.

The raw command behind it

If you would rather type it yourself, this is what the script effectively does:

tar
# pack
$ tar -I 'zstd -3 -T0' -cf /media/youruser/usbdiskname/backups/myproject.tar.zst myproject/

# unpack
$ tar -I zstd -xf myproject.tar.zst

# list the contents without extracting
$ tar -I zstd -tf myproject.tar.zst | less

Not maintained, change it yourself

This script was built for my own machine and is published as-is. It will not be updated, and I do not take bug reports or feature requests.

If you need it to do something else, hand the whole file to Claude, ChatGPT or another AI and ask in plain words: "put a date in the filename", "use gzip instead of zstd", "write to two disks in sequence", "email me when it finishes". The file is densely commented precisely so an AI can read it and understand why the code is the way it is. That is how it was written.