*! version 1.0.0 2026-07-20

Title

projectbackup -- Zip up a project folder, subfolders included, in native Stata

Syntax

projectbackup [folder] [, options]

folder is the directory to back up. If omitted, the current working
directory (c(pwd)) is used. Quote it if it contains spaces.

Where it goes
archive(dir) folder to hold the zip; default folder/_archive

prefix(str) zip-name prefix; default backup_foldername

replace overwrite an existing zip of the same name

Naming
timestamp append _YYYYMMDD_HHMMSS, so every backup is unique and sorts by date

label(str) insert a tag into the name, e.g. label(pre-refactor)

What goes in
exclude(patterns) top-level files/folders to skip; * and ? wildcards ok

nodotfiles skip top-level dot-entries (.git, .DS_Store, ...)

Log and manifest
toc create/append _backup_log.md in the archive folder

note(str) note recorded in the log row (turns on toc)

manifest write zipname_manifest.txt listing every archived file

Housekeeping
keep(#) after backing up, keep only the newest # timestamped backups

removeall delete all matching backups and make no new one

Stata Journal
zippy also save a .zippy copy (an email-safe .zip for SJ submissions)

Look before you leap
dryrun scan and report size/counts/path lengths only; write nothing

manifest (above) can be combined with dryrun once you commit to writing

force override the size / file-count safety stops

Description

projectbackup archives a folder and everything under it into a single
.zip, using nothing but Stata's own zipfile and Mata. There is no
shelling out, no tar, 7z, or PowerShell -- so the same command works
the same way on macOS, Windows, and Linux, and inside restricted environments
where you cannot run external programs.

It is built for the everyday "snapshot my project" habit: point it at a project
folder and it drops a dated zip into an _archive subfolder, optionally
keeping a running Markdown log of every backup you have taken. Before it writes
anything it walks the tree and warns you about the things that make zipping go
wrong -- a folder that is too big, too many files, or paths so deeply nested
that Windows will refuse to extract them.

Getting started

Back up the current project to ./_archive/backup_foldername.zip:

. projectbackup

Back up a specific folder, with a unique dated name and a log entry:

. projectbackup "~/Documents/MyProject", timestamp toc

Not sure how big it is? Measure first, write nothing:

. projectbackup "~/Documents/MyProject", dryrun

Options

Where it goes

archive(dir) is the folder the zip is written to. The default
is an _archive subfolder of the source folder, which is created if it does
not exist (its parent must already exist). projectbackup never packs its
own archive folder into the backup when that folder sits at the top level of the
source -- so your old backups are not nested inside each new one.

prefix(str) sets the leading part of the zip name. The default
is backup_ followed by the source folder's name. Spaces become
underscores.

replace overwrites an existing zip of the same name. Without it, a name
clash is an error (so a backup is never silently destroyed). With timestamp
you rarely need replace, because each name is unique.

Naming

timestamp appends _YYYYMMDD_HHMMSS to the name. This makes every
backup unique and -- crucially -- makes the names sort chronologically, which is
what keep() relies on.

label(str) inserts your own tag, e.g.
backup_myproj_pre-refactor.zip. Combine with timestamp to get both a
label and a date.

What goes in

exclude(patterns) lists top-level files or folders to leave out,
separated by spaces; * and ? wildcards are allowed and matching is
case-insensitive. Quoting the whole list or each pattern makes no difference:
exclude("data output .git") and exclude(data output .git) behave
identically. To match a name that contains a space, use a wildcard, e.g.
exclude(raw*) for a folder named raw data. Exclusions apply at the
top level only -- a subfolder named output nested three levels down is kept.

nodotfiles skips every top-level entry whose name starts with a dot
(.git, .Rhistory, .DS_Store, ...). Handy for excluding version
-control and OS clutter without naming each one.

Log and manifest

toc creates _backup_log.md in the archive folder if it does not
exist, then appends one row per backup (date, time, archive name, file count,
size, note). The log is a plain Markdown table, written with Stata's
file command, so it renders on GitHub and reads fine in any editor. See
The backup log.

note(str) records a free-text note in the log row and turns on
toc automatically. A pipe (|) in the note is replaced by a slash so
it cannot break the Markdown table.

manifest writes zipname_manifest.txt beside the zip: a sorted,
plain-text list of every file that went in (paths relative to the source
folder). Useful as a quick "what was in that backup" index without unzipping.

Housekeeping

keep(#) keeps only the newest # timestamped backups
for this prefix, deleting older ones (and their .zippy twins) right after a
successful backup. Only names carrying a _YYYYMMDD_HHMMSS stamp are
eligible, because only those sort by date; labelled or plain backups are never
pruned. Use keep() together with timestamp.

removeall deletes every backup (.zip and .zippy) that matches
the current prefix in the archive folder, and does not create a new one.
This is the cleanup counterpart to a normal run.

Stata Journal

zippy additionally saves the archive with a .zippy extension. The
Stata Journal asks authors
to rename .zip attachments (mail servers often strip .zip); a
.zippy file is a byte-for-byte copy that survives email, and the recipient
renames it back to .zip. See Stata Journal.

Look before you leap

dryrun does the full scan -- file count, total size, largest file, deepest
nesting, longest path -- and prints a report without writing anything. It
is the safe way to size up an unfamiliar or possibly huge folder.

force overrides the hard safety stops (see below). It has no effect on the
soft warnings, which never block a backup.

Safety checks

Before writing, projectbackup walks the whole tree in Mata and checks for
the conditions that make zipping fail, hang, or produce an archive that will not
open elsewhere. There are two tiers.

Hard stops (backup refused unless you add force):

total size > 2 GB Stata's zipfile is single-threaded and offers no compression tuning; multi-GB zips are slow and can fail to open in older unzip tools.

any file > 2 GB the classic ZIP format's per-file limit; large individual files are the usual culprit.

> 100,000 files the file count at which the walk and the zip both become impractically slow.

Soft warnings (printed, but the backup proceeds):

size > 500 MB "this may take a while."

> 20,000 files "this may take a while."

path > 200 chars Windows extraction may hit the 260-character MAX_PATH limit; the offending path is shown.

When a hard stop fires, projectbackup prints concrete advice: exclude the
heavy subfolders and back up code separately from data, split big subfolders into
their own zips, or reach for a dedicated archiver for genuinely multi-GB trees.
Run with dryrun to see all of this without committing.

The backup log

With toc (or note()), a Markdown table named _backup_log.md
accumulates in the archive folder, e.g.:

| date | time | archive | files | size | note |

|------|------|---------|-------|------|------|

| 20 Jul 2026 | 21:37:53 | backup_myproj_20260720_213753.zip | 128 | 4.2 MB | pre-refactor |

Newest rows are appended at the bottom. Because it is plain Markdown it renders
as a table on GitHub and stays readable as text everywhere else.

Stata Journal submissions (.zippy)

zippy is a convenience for authors submitting to the
Stata Journal. Their submission guidelines ask that .zip files be
renamed before emailing, because many mail systems block or quarantine
.zip attachments. projectbackup, ... zippy writes both the normal
.zip and an identical copy ending in .zippy; you attach the
.zippy, and the editor renames it back to .zip on receipt.

Stored results

projectbackup is rclass. A normal run stores:

Scalars

r(nfiles) number of files archived

r(ndirs) number of subfolders

r(bytes) total uncompressed size, in bytes

r(zipbytes) size of the written zip, in bytes

r(maxdepth) deepest nesting level

r(maxplen) longest relative path, in characters

r(seconds) wall-clock time taken

Macros

r(zipfile) full path of the zip written

r(zippy) full path of the .zippy copy (with zippy)

r(folder) source folder (absolute)

r(archive) archive folder (absolute)

r(size) human-readable total size

dryrun stores everything except r(zipfile), r(zipbytes), and
r(seconds). removeall stores r(archive) and
r(nremoved) (count of files deleted).

Examples

1. Simplest possible -- back up the current project

. projectbackup

2. Dated snapshot with a log entry

. projectbackup ".", timestamp toc

3. Measure a folder before committing to a backup

. projectbackup "~/BigStudy", dryrun

4. Back up code only -- skip data, output, and version control

. projectbackup, exclude(data output *.dta) nodotfiles timestamp

5. A named milestone snapshot

. projectbackup, label(before-reviewer-revisions) timestamp toc note(v1 submitted)

6. Rolling snapshots -- keep only the five most recent

. projectbackup, timestamp keep(5)

7. Send the archive somewhere outside the project

. projectbackup "~/Documents/MyProject", archive("~/Dropbox/backups") timestamp

8. A record of exactly what was archived

. projectbackup, manifest timestamp

9. Package a folder for a Stata Journal submission

. projectbackup "~/sj-package", zippy

10. Clear out every backup for this project

. projectbackup, removeall

Remarks

Paths inside the zip are stored relative to the source folder, so unzipping in an
empty directory reproduces the project's layout exactly (no absolute-path
folders). Zipping is done through a single zipfile call after the tree
has been scanned, so the file count and size you are warned about are the file
count and size that go in.

Because everything is native, projectbackup runs in the same command on any
platform Stata supports and needs no configuration. The one platform-specific
caveat is Windows' 260-character path limit on extraction, which the long-path
warning is there to catch in advance.

Author

Eric A. Booth. Feedback to eric.a.booth@gmail.com.

Also see

Online: zipfile, unzipfile, file, mkdir