A phone that records around the clock produces clips faster than anyone watches them, so sooner or later the folder they land in on a laptop or a small home server gets a cleanup job. The usual line is short:
find ~/clips -name '*.mp4' -mtime +7 -delete
Most people read it as: delete clips older than a week. I built a folder of fake clips with modification times placed an hour either side of each day boundary and ran that line, some variations of it, and two common mistakes around it. In short:
-
-mtime +7keeps clips for eight days, not seven. find counts whole 24-hour periods and drops the remainder, so a clip 191 hours old counts as 7 days, and 7 is not more than 7. Only the 193-hour clip matched. -
-deleteplaced first removed the whole folder, including a text file that is not a clip. find exited with status 1, after the deleting was done. -
A plain
cp -rto a backup drive reset every clip's age to zero, so the same cleanup on the copy matched none of them. - An unset folder variable, left unquoted, sent find to the current directory, where it matched clips in a backup folder it was not pointed at.
Disclosure: I make an Android camera app. Nothing here uses its code or describes how it names or stores files. Everything below runs on a computer you copy clips to, against files the scripts create themselves.
The test folder
Eight fake clips, 1 KiB of random bytes each, with modification times set to 23, 25, 47, 49, 167, 169, 191 and 193 hours ago, plus one notes.txt that no cleanup should touch. Each test below rebuilds the folder first, so no result depends on the one before it.
#!/bin/bash
# Build a folder of fake clips whose modification times sit just either side of
# the day boundaries a cleanup job cares about.
set -e
rm -rf clips && mkdir clips
now=$(date +%s)
for h in 23 25 47 49 167 169 191 193; do
f="clips/clip_${h}h_old.mp4"
head -c 1024 /dev/urandom > "$f"
touch -d "@$(( now - h*3600 ))" "$f"
done
echo "keep me" > clips/notes.txt
ls -l --time-style=+%Y-%m-%dT%H:%M clips | tail -n +2 | awk '{print $6, $7}'
What -mtime actually counts
The man page defines -mtime n by pointing to -atime, and the rule there is the whole story: "When find figures out how many 24-hour periods ago the file was last accessed, any fractional part is ignored, so to match -atime +1, a file has to have been accessed at least two days ago."
So find turns a clip's age into a whole number of days by rounding down, then compares. A clip 191 hours old is 7.96 days, which becomes 7, and 7 does not pass a greater-than-7 test. Here are the variations I ran:
#!/bin/bash
# Each test runs against a fresh copy of the folder from setup.sh.
run() { echo "\$ $1"; ./setup.sh >/dev/null; eval "$1" | sort; echo; }
run "find clips -name '*.mp4' -mtime +7"
run "find clips -name '*.mp4' -mtime 7"
run "find clips -name '*.mp4' -mtime +6"
run "find clips -name '*.mp4' -mtime -1"
run "find clips -name '*.mp4' -mmin +\$((7*24*60))"
run "find clips -name '*.mp4' ! -newermt '7 days ago'"
$ find clips -name '*.mp4' -mtime +7
clips/clip_193h_old.mp4
$ find clips -name '*.mp4' -mtime 7
clips/clip_169h_old.mp4
clips/clip_191h_old.mp4
$ find clips -name '*.mp4' -mtime +6
clips/clip_169h_old.mp4
clips/clip_191h_old.mp4
clips/clip_193h_old.mp4
$ find clips -name '*.mp4' -mtime -1
clips/clip_23h_old.mp4
$ find clips -name '*.mp4' -mmin +$((7*24*60))
clips/clip_169h_old.mp4
clips/clip_191h_old.mp4
clips/clip_193h_old.mp4
$ find clips -name '*.mp4' ! -newermt '7 days ago'
clips/clip_169h_old.mp4
clips/clip_191h_old.mp4
clips/clip_193h_old.mp4
Reading the results against the ages:
| Test | Matched | What it really means |
|---|---|---|
-mtime +7 |
193 h | at least 8 full days (192 h) |
-mtime 7 |
169 h, 191 h | from 168 h up to, but not including, 192 h |
-mtime +6 |
169 h, 191 h, 193 h | at least 7 full days |
-mtime -1 |
23 h | under 24 h |
-mmin +10080 |
169 h, 191 h, 193 h | more than 10080 minutes (7 × 24 × 60) |
! -newermt '7 days ago' |
169 h, 191 h, 193 h | not modified after the moment 7 days ago |
The 167-hour clip is the useful edge: it is 6.96 days old, which rounds to 6, so even -mtime +6 leaves it alone.
None of this is a bug. It is documented, and a retention period one day longer than intended is a quiet failure: the folder holds about a seventh more footage than the disk was sized for, and nothing ever reports it. If a week has to mean 168 hours, -mmin +10080 and ! -newermt '7 days ago' both say exactly that. The man page notes that -newermt takes its time string the way GNU date -d does.
One more option changes the answer: -daystart makes the age tests measure "from the beginning of today rather than from 24 hours ago". I did not test it here. A cleanup that runs at different times of day gets different cutoffs with it, which is a separate question from rounding.
Where -delete goes on the line
find reads its arguments as one expression, evaluated left to right for each file, and it stops as soon as the answer is known. The man page states the consequence under -delete: "putting -delete first will make find try to delete everything below the starting points you specified."
I ran the same three parts in two orders:
#!/bin/bash
# Same tests, same folder, two orders. Prints what is left afterwards.
./setup.sh >/dev/null
find clips -name '*.mp4' -mtime +7 -delete
echo "-delete last -> exit $?, left: $(ls clips | tr '\n' ' ')"
./setup.sh >/dev/null
find clips -delete -name '*.mp4' -mtime +7 2>errors.txt
echo "-delete first -> exit $?, $(wc -l < errors.txt) error lines, left: $(ls clips 2>&1)"
-delete last -> exit 0, left: clip_167h_old.mp4 clip_169h_old.mp4 clip_191h_old.mp4 clip_23h_old.mp4 clip_25h_old.mp4 clip_47h_old.mp4 clip_49h_old.mp4 notes.txt
-delete first -> exit 1, 8 error lines, left: ls: cannot access 'clips': No such file or directory
With -delete last, only the 193-hour clip went (with +7 again, so the eight-day rule applies). With -delete first, every file was removed before find looked at a name or an age, notes.txt with them, and then the emptied clips folder itself. The 8 error lines came afterwards, from the tests after -delete trying to read files that were already gone. An exit status of 1 was the one sign that anything went wrong, and it arrived too late to help.
That makes the order of a dry run matter. The man page suggests testing "with -print prior to adding -delete", and adds a caution: -delete turns on -depth, so "it is usually best to remember to use -depth explicitly during those earlier test runs." A dry run that lists files in a different order from the real run is a weaker check than it looks.
Copying a clip resets its age
A cleanup that runs on a backup drive depends on the copies keeping their modification times. A plain cp does not keep them; cp -p does.
#!/bin/bash
# Copy the folder to a "backup drive" two ways, then run the same cleanup test.
./setup.sh >/dev/null
rm -rf plain keep
cp -r clips plain
cp -rp clips keep
echo "plain cp -r : $(find plain -name '*.mp4' -mtime +6 | wc -l) clips match -mtime +6"
echo "cp -rp : $(find keep -name '*.mp4' -mtime +6 | wc -l) clips match -mtime +6"
plain cp -r : 0 clips match -mtime +6
cp -rp : 3 clips match -mtime +6
After a plain copy, every clip on the backup looked brand new, so a keep-one-week job on that drive would not delete anything and the drive would just fill up. An earlier piece on timestamps and time-range retrieval argues for treating the filename, not the modification time, as the index for exactly this reason. The test above shows what that looks like from the cleanup side. I only tested GNU cp. Other copy tools, sync apps and phone transfer modes have their own rules, so check the modification times on the far side once before trusting any age-based job there.
The folder variable nobody set
Cleanup lines usually live in a script with the folder in a variable, and a typo in the variable name is easy to make. The man page says what happens when find gets no starting point: "If no starting-point is specified, `.' is assumed."
`bash
!/bin/bash
The folder variable is CLIPS_DIR; the cleanup line has a typo, CLIP_DIR.
A second folder, backup/, sits next to clips/ and is never named.
./setup.sh >/dev/null
rm -rf backup && cp -rp clips backup
CLIPS_DIR="$PWD/clips"
echo "unquoted: $(find $CLIP_DIR -name '.mp4' -mtime +6 | sort | tr '\n' ' ')"
echo "quoted: $(find "$CLIP_DIR" -name '.mp4' -mtime +6 2>&1)"
echo "guarded: $( (find "${CLIP_DIR:?is not set}" -name '*.mp4' -mtime +6) 2>&1)"
`
text
unquoted: ./backup/clip_169h_old.mp4 ./backup/clip_191h_old.mp4 ./backup/clip_193h_old.mp4 ./clips/clip_169h_old.mp4 ./clips/clip_191h_old.mp4 ./clips/clip_193h_old.mp4
quoted: find: ‘’: No such file or directory
guarded: ./unset_dir.sh: line 9: CLIP_DIR: is not set
Unquoted, the empty variable disappears from the command, so find searched the directory the script was run from and matched the three old clips in backup/ as well as in clips/. With -delete on the end, that line would have cleaned out a folder nobody named. Quoting the variable turned it into an empty path, which find refused. ${CLIP_DIR:?} stopped the script before find ran at all, and that is the version to use for anything that deletes.
A cleanup script that lists before it deletes
Putting the four results together:
`bash
!/bin/bash
Delete .mp4 clips older than KEEP_DAYS from CLIPS_DIR.
Lists by default; deletes only when run with --delete.
set -u
dir="${CLIPS_DIR:?CLIPS_DIR is not set}"
days="${KEEP_DAYS:?KEEP_DAYS is not set}"
[ -d "$dir" ] || { echo "no such folder: $dir" >&2; exit 1; }
minutes=$(( days * 24 * 60 ))
matches=$(find "$dir" -maxdepth 1 -type f -name '.mp4' -mmin +"$minutes")
count=$(printf '%s' "$matches" | grep -c . || true)
total=$(find "$dir" -maxdepth 1 -type f -name '.mp4' | wc -l)
echo "$count of $total clips are older than $days days"
printf '%s\n' "$matches" | grep . || true
if [ "${1:-}" = "--delete" ]; then
find "$dir" -maxdepth 1 -type f -name '.mp4' -mmin +"$minutes" -delete
echo "deleted; $(find "$dir" -maxdepth 1 -type f -name '.mp4' | wc -l) clips remain"
fi
`
What each choice is for:
-
${CLIPS_DIR:?}and${KEEP_DAYS:?}stop the script if either is missing. -
-mmin +$minutesmeans exactly the number of days you set, with no hidden extra day. -
-maxdepth 1 -type f -name '*.mp4'limits it to clip files directly inside the folder, so subfolders and other file types are left alone. -
-deletesits last, after every test. - Without
--deleteit only lists. The list and the delete use the same tests, and with-maxdepth 1 -type fthere are no subfolders for-depthto reorder, so the dry run names the same files the delete removes.
`text
$ CLIPS_DIR=clips KEEP_DAYS=7 ./cleanup.sh
3 of 8 clips are older than 7 days
clips/clip_169h_old.mp4
clips/clip_193h_old.mp4
clips/clip_191h_old.mp4
$ CLIPS_DIR=clips KEEP_DAYS=7 ./cleanup.sh --delete
3 of 8 clips are older than 7 days
clips/clip_169h_old.mp4
clips/clip_193h_old.mp4
clips/clip_191h_old.mp4
deleted; 5 clips remain
$ KEEP_DAYS=7 ./cleanup.sh --delete
./cleanup.sh: line 5: CLIPS_DIR: CLIPS_DIR is not set
exit status 1
`
One honest gap: the list and the delete are two separate find runs, so a clip that crosses the age line between them is deleted without being listed. For a daily job that window is seconds wide, but the printed list is a close preview, not a promise.
What I did not test
All of this ran with GNU findutils 4.8.0 and GNU coreutils 8.32 on Linux. macOS ships a BSD find and cp, and I did not run these scripts there, so check its man page before reusing any line on a Mac. I also did not test how memory cards or network shares report modification times, so look at a few ls -l times there before relying on them.
Before turning on any age-based delete
- Run the line without
-deleteand read the list. - Check that the oldest clip you expect to keep is not on it, and the newest one you expect to lose is.
- Use
-mminor-newermtwhen the number of days has to be exact. - Put
-deletelast, and guard the folder variable with${VAR:?}. - If the job runs on a copy, check the copy's modification times once first.
Try it: Background Camera RemoteStream on Google Play -- record with the screen off and keep the footage on the phone. Setup notes and other write-ups are at superfunicular.com.
Sources. Quotes are from the find(1) manual page for GNU findutils. Test results come from the scripts above, run as shown. The age test decides what a cleanup deletes, and only reading its list first tells you whether that is what you meant.
Top comments (0)