Skip to content

Instantly share code, notes, and snippets.

@v1ne
Created April 27, 2020 19:59
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save v1ne/95129850d44b15ae300a38b3b602c3b6 to your computer and use it in GitHub Desktop.
Save v1ne/95129850d44b15ae300a38b3b602c3b6 to your computer and use it in GitHub Desktop.
Shrink a cache directory to a given size
#!/usr/bin/env rdmd
/*
* BSD 2-Clause License
*
* Copyright (c) 2020, v1ne <v1ne2go@gmail.com>
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
*
* 1. Redistributions of source code must retain the above copyright notice, this
* list of conditions and the following disclaimer.
*
* 2. Redistributions in binary form must reproduce the above copyright notice,
* this list of conditions and the following disclaimer in the documentation
* and/or other materials provided with the distribution.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
* IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
* DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
* FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
* DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
* SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
* CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
* OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
* OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
module pruneCache;
immutable auto programName = "prune-cache.d";
immutable auto programDescription =
"Prunes a cache directory to the set of recently used files that does not
exceed <size> bytes, e.g. 10GB. If your file system does not provide
access times, modification times are used.
Options:";
import core.stdc.stdlib: exit;
import std.algorithm.iteration;
import std.algorithm.searching: until;
import std.algorithm.sorting: sort;
import std.array;
import std.conv: parse, to;
import std.file: dirEntries, remove, FileException, SpanMode;
import std.getopt;
import std.math: isFinite;
import std.path: buildPath;
import std.stdio;
import std.string: format;
import std.typecons: No, Yes;
struct FileInfo {
string path;
ulong size;
long lastAccessed;
};
int main(string[] args) {
bool dryRun;
uint verbose;
auto result = getopt(args,
std.getopt.config.caseSensitive,
std.getopt.config.bundling,
"dry-run|n", "Only show which files would be deleted", &dryRun,
"verbose|v+", "Print details on what happens (more shows more details)", &verbose);
bool parmsOk = args.length == 2 + 1 && !args[1].empty && !args[2].empty;
if(result.helpWanted || !parmsOk) {
writeln("Usage: %s [-%s] directory size".format(
programName, result.options.map!(opt => opt.optShort[1])));
defaultGetoptPrinter(programDescription, result.options);
return parmsOk ? 0 : 1;
}
auto targetDir = args[1];
auto targetDirSize = parseDirSize(args[2]);
if(verbose > 0)
writeln(dryRun ? "Would be shrinking " : "Shrinking ",
targetDir, " to ", toSiSuffixed(targetDirSize), "B.");
auto fileInfos = dirEntries(targetDir, SpanMode.depth, No.followSymlinks)
.filter!(f => f.isFile && !f.isSymlink)
.map!(f => FileInfo(f.name, f.size, f.timeLastAccessed.stdTime))
.array;
fileInfos.sort!((f, g) => f.lastAccessed > g.lastAccessed);
if(verbose > 2) {
writeln("Sorted files: ");
foreach(fi; fileInfos)
writeln(" ", fi.path, ": @", fi.lastAccessed, ", size ", toSiSuffixed(fi.size));
}
ulong sizeLeft = targetDirSize;
uint i = 0;
foreach(FileInfo fi; fileInfos) {
if(sizeLeft < fi.size)
break;
sizeLeft -= fi.size;
i++;
}
auto toKeep = fileInfos[0..i];
auto toRemove = fileInfos[i..$];
if(verbose > 2) {
writeln("Files to keep:");
foreach(fi; toKeep)
writeln(" ", fi.path);
}
bool success = true;
ulong actuallyRemovedBytes = 0;
ulong actuallyRemoved = 0;
foreach(fi; toRemove) {
if(verbose > 1)
writeln(dryRun ? "Would remove " : "Removing ", fi.path);
if(!dryRun) {
try {
remove(buildPath(targetDir, fi.path));
actuallyRemovedBytes += fi.size;
actuallyRemoved++;
} catch(FileException e) {
stdout.flush();
stderr.writeln(e.msg);
success = false;
}
} else {
actuallyRemovedBytes += fi.size;
actuallyRemoved++;
}
}
if(verbose > 0) {
auto sizeTotal = fileInfos.map!(fi => fi.size).sum;
writeln("%s %sB (%.2g%%) in %d files (%.2g%%), keeping %sB".format(
dryRun ? "Would free" : "Freed",
toSiSuffixed(actuallyRemovedBytes),
100. * actuallyRemovedBytes / (1e-9 + sizeTotal),
actuallyRemoved,
100. * actuallyRemoved / (1e-9 + fileInfos.length),
toSiSuffixed(sizeTotal - actuallyRemovedBytes)));
}
return success ? 0 : 1;
}
ulong parseDirSize(string str) {
auto input = str;
double amount = parse!double(str);
double multiplier = 1;
if(!str.empty && str[$-1] == 'B')
str = str[0..$-1];
if(str.length == 1) {
switch(str[0]) {
case 'k':
multiplier = 1000.;
break;
case 'M':
multiplier = 1000*1000.;
break;
case 'G':
multiplier = 1000*1000*1000.;
break;
case 'T':
multiplier = 1000*1000*1000*1000.;
break;
case 'P':
multiplier = 1000*1000*1000*1000*1000.;
break;
default:
stderr.writeln("Unknown SI unit prefix: ", str[0]);
exit(-1);
}
} else if(!str.empty) {
stderr.writeln("Unable to parse number: ", input);
exit(-1);
}
amount *= multiplier;
if(!(amount >= 0. && amount.isFinite)) {
stderr.writeln("Invalid number: ", input);
exit(-1);
}
return amount.to!ulong;
}
string toSiSuffixed(ulong number) {
string[] suffixes = [" ", " k", " M", " G", " T", " P"];
double num = number;
uint i = 0;
while (num >= 1000. && i < suffixes.length) {
++i;
num /= 1000;
}
return "%.3g%s".format(num, suffixes[i]);
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment