mirror of
https://code.blicky.net/yorhel/ncdu.git
synced 2026-01-13 01:08:41 -09:00
Implement directory refresh
This complicated the scan code more than I had anticipated and has a few inherent bugs with respect to calculating shared hardlink sizes. Still, the merge approach avoids creating a full copy of the subtree, so that's another memory usage related win compared to the C version. On the other hand, it does leak memory if nodes can't be reused. Not quite as well tested as I should have, so I'm sure there's bugs.
This commit is contained in:
parent
ff3e3bccc6
commit
6c2ab5001c
7 changed files with 370 additions and 88 deletions
|
|
@ -29,7 +29,6 @@ backported to the C version, depending on how viable a proper Zig release is.
|
|||
Missing features:
|
||||
|
||||
- Help window
|
||||
- Directory refresh
|
||||
- File deletion
|
||||
- Opening a shell
|
||||
|
||||
|
|
@ -43,6 +42,7 @@ Already implemented:
|
|||
- Using separate structs for directory, file and hard link nodes, each storing
|
||||
only the information necessary for that particular type of node.
|
||||
- Using an arena allocator and getting rid of data alignment.
|
||||
- Refreshing a directory no longer creates a full copy of the (sub)tree.
|
||||
- Improved performance of hard link counting (fixing
|
||||
[#121](https://code.blicky.net/yorhel/ncdu/issues/121)).
|
||||
- Add support for separate counting hard links that are shared with other
|
||||
|
|
@ -70,12 +70,14 @@ Aside from this implementation being unfinished:
|
|||
the in-memory directory tree.
|
||||
- Not nearly as well tested.
|
||||
- Directories that could not be opened are displayed as files.
|
||||
- The disk usage of directory entries themselves is not updated during refresh.
|
||||
|
||||
### Minor UI differences
|
||||
|
||||
Not sure if these count as improvements or regressions, so I'll just list these
|
||||
separately:
|
||||
|
||||
- The browsing UI is not visible during refresh.
|
||||
- Some columns in the file browser are hidden automatically if the terminal is
|
||||
not wide enough to display them.
|
||||
- Browsing keys other than changing the currently selected item don't work
|
||||
|
|
|
|||
26
doc/ncdu.pod
26
doc/ncdu.pod
|
|
@ -411,25 +411,25 @@ directory, as some inodes may still be accessible from hard links outside it.
|
|||
|
||||
=head1 BUGS
|
||||
|
||||
Directory hard links are not supported. They will not be detected as being hard
|
||||
links, and will thus be scanned and counted multiple times.
|
||||
Directory hard links and firmlinks (MacOS) are not supported. They will not be
|
||||
detected as being hard links, and may thus be scanned and counted multiple
|
||||
times.
|
||||
|
||||
Some minor glitches may appear when displaying filenames that contain multibyte
|
||||
or multicolumn characters.
|
||||
|
||||
The unique and shared directory sizes are calculated based on the assumption
|
||||
that the link count of hard links does not change during a filesystem scan or
|
||||
in between refreshes. If it does, for example after deleting a hard link, then
|
||||
these numbers will be very much incorrect and a full refresh by restarting ncdu
|
||||
is needed to get correct numbers again.
|
||||
|
||||
All sizes are internally represented as a signed 64bit integer. If you have a
|
||||
directory larger than 8 EiB minus one byte, ncdu will clip its size to 8 EiB
|
||||
minus one byte. When deleting items in a directory with a clipped size, the
|
||||
resulting sizes will be incorrect.
|
||||
|
||||
Item counts are stored in a signed 32-bit integer without overflow detection.
|
||||
If you have a directory with more than 2 billion files, quite literally
|
||||
anything can happen.
|
||||
|
||||
On macOS 10.15 and later, running ncdu on the root directory without
|
||||
`--exclude-firmlinks` may cause directories to be scanned and counted multiple
|
||||
times. Firmlink cycles are currently (1.15.1) not detected, so it may also
|
||||
cause ncdu to get stuck in an infinite loop and eventually run out of memory.
|
||||
minus one byte. When deleting or refreshing items in a directory with a clipped
|
||||
size, the resulting sizes will be incorrect. Likewise, item counts are stored
|
||||
in a 32-bit integer, so will be incorrect in the unlikely event that you happen
|
||||
to have more than 4 billion items in a directory.
|
||||
|
||||
Please report any other bugs you may find at the bug tracker, which can be
|
||||
found on the web site at https://dev.yorhel.nl/ncdu
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
const std = @import("std");
|
||||
const main = @import("main.zig");
|
||||
const model = @import("model.zig");
|
||||
const scan = @import("scan.zig");
|
||||
const ui = @import("ui.zig");
|
||||
const c = @cImport(@cInclude("time.h"));
|
||||
usingnamespace @import("util.zig");
|
||||
|
|
@ -664,6 +665,14 @@ pub fn keyInput(ch: i32) void {
|
|||
switch (ch) {
|
||||
'q' => if (main.config.confirm_quit) { state = .quit; } else ui.quit(),
|
||||
'i' => info.set(dir_items.items[cursor_idx], .info),
|
||||
'r' => {
|
||||
if (main.config.imported) {
|
||||
// TODO: Display message
|
||||
} else {
|
||||
main.state = .refresh;
|
||||
scan.setupRefresh(dir_parents.copy());
|
||||
}
|
||||
},
|
||||
|
||||
// Sort & filter settings
|
||||
'n' => sortToggle(.name, .asc),
|
||||
|
|
|
|||
26
src/main.zig
26
src/main.zig
|
|
@ -30,6 +30,8 @@ var allocator_state = std.mem.Allocator{
|
|||
.resizeFn = wrapResize,
|
||||
};
|
||||
pub const allocator = &allocator_state;
|
||||
//var general_purpose_allocator = std.heap.GeneralPurposeAllocator(.{}){};
|
||||
//pub const allocator = &general_purpose_allocator.allocator;
|
||||
|
||||
pub const config = struct {
|
||||
pub const SortCol = enum { name, blocks, size, items, mtime };
|
||||
|
|
@ -65,7 +67,7 @@ pub const config = struct {
|
|||
pub var confirm_quit: bool = false;
|
||||
};
|
||||
|
||||
pub var state: enum { scan, browse } = .browse;
|
||||
pub var state: enum { scan, browse, refresh } = .scan;
|
||||
|
||||
// Simple generic argument parser, supports getopt_long() style arguments.
|
||||
// T can be any type that has a 'fn next(T) ?[:0]const u8' method, e.g.:
|
||||
|
|
@ -257,7 +259,6 @@ pub fn main() void {
|
|||
|
||||
event_delay_timer = std.time.Timer.start() catch unreachable;
|
||||
defer ui.deinit();
|
||||
state = .scan;
|
||||
|
||||
var out_file = if (export_file) |f| (
|
||||
if (std.mem.eql(u8, f, "-")) std.io.getStdOut()
|
||||
|
|
@ -265,9 +266,11 @@ pub fn main() void {
|
|||
catch |e| ui.die("Error opening export file: {s}.\n", .{ui.errorString(e)})
|
||||
) else null;
|
||||
|
||||
if (import_file) |f| scan.importRoot(f, out_file)
|
||||
else scan.scanRoot(scan_dir orelse ".", out_file)
|
||||
catch |e| ui.die("Error opening directory: {s}.\n", .{ui.errorString(e)});
|
||||
if (import_file) |f| {
|
||||
scan.importRoot(f, out_file);
|
||||
config.imported = true;
|
||||
} else scan.scanRoot(scan_dir orelse ".", out_file)
|
||||
catch |e| ui.die("Error opening directory: {s}.\n", .{ui.errorString(e)});
|
||||
if (out_file != null) return;
|
||||
|
||||
config.scan_ui = .full; // in case we're refreshing from the UI, always in full mode.
|
||||
|
|
@ -275,7 +278,14 @@ pub fn main() void {
|
|||
state = .browse;
|
||||
browser.loadDir();
|
||||
|
||||
while (true) handleEvent(true, false);
|
||||
while (true) {
|
||||
if (state == .refresh) {
|
||||
scan.scan();
|
||||
state = .browse;
|
||||
browser.loadDir();
|
||||
} else
|
||||
handleEvent(true, false);
|
||||
}
|
||||
}
|
||||
|
||||
var event_delay_timer: std.time.Timer = undefined;
|
||||
|
|
@ -286,7 +296,7 @@ pub fn handleEvent(block: bool, force_draw: bool) void {
|
|||
if (block or force_draw or event_delay_timer.read() > config.update_delay) {
|
||||
if (ui.inited) _ = ui.c.erase();
|
||||
switch (state) {
|
||||
.scan => scan.draw(),
|
||||
.scan, .refresh => scan.draw(),
|
||||
.browse => browser.draw(),
|
||||
}
|
||||
if (ui.inited) _ = ui.c.refresh();
|
||||
|
|
@ -303,7 +313,7 @@ pub fn handleEvent(block: bool, force_draw: bool) void {
|
|||
if (ch == 0) return;
|
||||
if (ch == -1) return handleEvent(firstblock, true);
|
||||
switch (state) {
|
||||
.scan => scan.keyInput(ch),
|
||||
.scan, .refresh => scan.keyInput(ch),
|
||||
.browse => browser.keyInput(ch),
|
||||
}
|
||||
firstblock = false;
|
||||
|
|
|
|||
|
|
@ -13,6 +13,9 @@ var allocator = std.heap.ArenaAllocator.init(std.heap.page_allocator);
|
|||
|
||||
pub const EType = packed enum(u2) { dir, link, file };
|
||||
|
||||
// Type for the Entry.blocks field. Smaller than a u64 to make room for flags.
|
||||
pub const Blocks = u60;
|
||||
|
||||
// Memory layout:
|
||||
// Dir + name (+ alignment + Ext)
|
||||
// or: Link + name (+ alignment + Ext)
|
||||
|
|
@ -31,7 +34,8 @@ pub const EType = packed enum(u2) { dir, link, file };
|
|||
pub const Entry = packed struct {
|
||||
etype: EType,
|
||||
isext: bool,
|
||||
blocks: u61, // 512-byte blocks
|
||||
counted: bool, // Whether or not this entry's size has been counted in its parents
|
||||
blocks: Blocks, // 512-byte blocks
|
||||
size: u64,
|
||||
next: ?*Entry,
|
||||
|
||||
|
|
@ -107,7 +111,10 @@ pub const Entry = packed struct {
|
|||
}
|
||||
}
|
||||
|
||||
fn addStats(self: *Entry, parents: *const Parents) void {
|
||||
pub fn addStats(self: *Entry, parents: *const Parents) void {
|
||||
if (self.counted) return;
|
||||
self.counted = true;
|
||||
|
||||
const dev = parents.top().dev;
|
||||
// Set if this is the first time we've found this hardlink in the bottom-most directory of the given dev.
|
||||
// Means we should count it for other-dev parent dirs, too.
|
||||
|
|
@ -154,6 +161,64 @@ pub const Entry = packed struct {
|
|||
}
|
||||
}
|
||||
|
||||
// Opposite of addStats(), but has some limitations:
|
||||
// - shared_* parent sizes are not updated; there's just no way to
|
||||
// correctly adjust these without a full rescan of the tree
|
||||
// - If addStats() saturated adding sizes, then the sizes after delStats()
|
||||
// will be incorrect.
|
||||
// - mtime of parents is not adjusted (but that's a feature, possibly?)
|
||||
//
|
||||
// The first point can be relaxed so that a delStats() followed by
|
||||
// addStats() with the same data will not result in broken shared_*
|
||||
// numbers, but for now the easy (and more efficient) approach is to try
|
||||
// and avoid using delStats() when not strictly necessary.
|
||||
//
|
||||
// This function assumes that, for directories, all sub-entries have
|
||||
// already been un-counted.
|
||||
pub fn delStats(self: *Entry, parents: *const Parents) void {
|
||||
if (!self.counted) return;
|
||||
self.counted = false;
|
||||
|
||||
const dev = parents.top().dev;
|
||||
var del_hl = false;
|
||||
|
||||
var it = parents.iter();
|
||||
while(it.next()) |p| {
|
||||
var del_total = false;
|
||||
p.items = saturateSub(p.items, 1);
|
||||
|
||||
if (self.etype == .link and dev != p.dev) {
|
||||
del_total = del_hl;
|
||||
} else if (self.link()) |l| {
|
||||
const n = devices.HardlinkNode{ .ino = l.ino, .dir = p };
|
||||
var dp = devices.list.items[dev].hardlinks.getEntry(n);
|
||||
if (dp) |d| {
|
||||
d.value_ptr.* -= 1;
|
||||
del_total = d.value_ptr.* == 0;
|
||||
del_hl = del_total;
|
||||
if (del_total)
|
||||
_ = devices.list.items[dev].hardlinks.remove(n);
|
||||
}
|
||||
} else
|
||||
del_total = true;
|
||||
if(del_total) {
|
||||
p.entry.size = saturateSub(p.entry.size, self.size);
|
||||
p.entry.blocks = saturateSub(p.entry.blocks, self.blocks);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn delStatsRec(self: *Entry, parents: *Parents) void {
|
||||
if (self.dir()) |d| {
|
||||
parents.push(d);
|
||||
var it = d.sub;
|
||||
while (it) |e| : (it = e.next)
|
||||
e.delStatsRec(parents);
|
||||
parents.pop();
|
||||
}
|
||||
self.delStats(parents);
|
||||
}
|
||||
|
||||
// Insert this entry into the tree at the given directory, updating parent sizes and item counts.
|
||||
pub fn insert(self: *Entry, parents: *const Parents) void {
|
||||
self.next = parents.top().sub;
|
||||
|
|
@ -220,6 +285,14 @@ pub const File = packed struct {
|
|||
_pad: u3,
|
||||
|
||||
name: u8,
|
||||
|
||||
pub fn resetFlags(f: *@This()) void {
|
||||
f.err = false;
|
||||
f.excluded = false;
|
||||
f.other_fs = false;
|
||||
f.kernfs = false;
|
||||
f.notreg = false;
|
||||
}
|
||||
};
|
||||
|
||||
pub const Ext = packed struct {
|
||||
|
|
|
|||
314
src/scan.zig
314
src/scan.zig
|
|
@ -9,7 +9,7 @@ const c_fnmatch = @cImport(@cInclude("fnmatch.h"));
|
|||
|
||||
// Concise stat struct for fields we're interested in, with the types used by the model.
|
||||
const Stat = struct {
|
||||
blocks: u61 = 0,
|
||||
blocks: model.Blocks = 0,
|
||||
size: u64 = 0,
|
||||
dev: u64 = 0,
|
||||
ino: u64 = 0,
|
||||
|
|
@ -100,6 +100,155 @@ fn writeJsonString(wr: anytype, s: []const u8) !void {
|
|||
try wr.writeByte('"');
|
||||
}
|
||||
|
||||
// A ScanDir represents an in-memory directory listing (i.e. model.Dir) where
|
||||
// entries read from disk can be merged into, without doing an O(1) lookup for
|
||||
// each entry.
|
||||
const ScanDir = struct {
|
||||
// Lookup table for name -> *entry.
|
||||
// null is never stored in the table, but instead used pass a name string
|
||||
// as out-of-band argument for lookups.
|
||||
entries: Map,
|
||||
const Map = std.HashMap(?*model.Entry, void, HashContext, 80);
|
||||
|
||||
const HashContext = struct {
|
||||
cmp: []const u8 = "",
|
||||
|
||||
pub fn hash(self: @This(), v: ?*model.Entry) u64 {
|
||||
return std.hash.Wyhash.hash(0, if (v) |e| @as([]const u8, e.name()) else self.cmp);
|
||||
}
|
||||
|
||||
pub fn eql(self: @This(), ap: ?*model.Entry, bp: ?*model.Entry) bool {
|
||||
if (ap == bp) return true;
|
||||
const a = if (ap) |e| @as([]const u8, e.name()) else self.cmp;
|
||||
const b = if (bp) |e| @as([]const u8, e.name()) else self.cmp;
|
||||
return std.mem.eql(u8, a, b);
|
||||
}
|
||||
};
|
||||
|
||||
const Self = @This();
|
||||
|
||||
fn init(parents: *const model.Parents) Self {
|
||||
var self = Self{ .entries = Map.initContext(main.allocator, HashContext{}) };
|
||||
|
||||
var count: Map.Size = 0;
|
||||
var it = parents.top().sub;
|
||||
while (it) |e| : (it = e.next) count += 1;
|
||||
self.entries.ensureCapacity(count) catch unreachable;
|
||||
|
||||
it = parents.top().sub;
|
||||
while (it) |e| : (it = e.next)
|
||||
self.entries.putAssumeCapacity(e, @as(void,undefined));
|
||||
return self;
|
||||
}
|
||||
|
||||
fn addSpecial(self: *Self, parents: *model.Parents, name: []const u8, t: Context.Special) void {
|
||||
var e = blk: {
|
||||
if (self.entries.getEntryAdapted(@as(?*model.Entry,null), HashContext{ .cmp = name })) |entry| {
|
||||
// XXX: If the type doesn't match, we could always do an
|
||||
// in-place conversion to a File entry. That's more efficient,
|
||||
// but also more code. I don't expect this to happen often.
|
||||
var e = entry.key_ptr.*.?;
|
||||
if (e.etype == .file) {
|
||||
if (e.size > 0 or e.blocks > 0) {
|
||||
e.delStats(parents);
|
||||
e.size = 0;
|
||||
e.blocks = 0;
|
||||
e.addStats(parents);
|
||||
}
|
||||
e.file().?.resetFlags();
|
||||
_ = self.entries.removeAdapted(@as(?*model.Entry,null), HashContext{ .cmp = name });
|
||||
break :blk e;
|
||||
} else e.delStatsRec(parents);
|
||||
}
|
||||
var e = model.Entry.create(.file, false, name);
|
||||
e.next = parents.top().sub;
|
||||
parents.top().sub = e;
|
||||
e.addStats(parents);
|
||||
break :blk e;
|
||||
};
|
||||
var f = e.file().?;
|
||||
switch (t) {
|
||||
.err => e.set_err(parents),
|
||||
.other_fs => f.other_fs = true,
|
||||
.kernfs => f.kernfs = true,
|
||||
.excluded => f.excluded = true,
|
||||
}
|
||||
}
|
||||
|
||||
fn addStat(self: *Self, parents: *model.Parents, name: []const u8, stat: *Stat) *model.Entry {
|
||||
const etype = if (stat.dir) model.EType.dir
|
||||
else if (stat.hlinkc) model.EType.link
|
||||
else model.EType.file;
|
||||
var e = blk: {
|
||||
if (self.entries.getEntryAdapted(@as(?*model.Entry,null), HashContext{ .cmp = name })) |entry| {
|
||||
// XXX: In-place conversion may also be possible here.
|
||||
var e = entry.key_ptr.*.?;
|
||||
// changes of dev/ino affect hard link counting in a way we can't simple merge.
|
||||
const samedev = if (e.dir()) |d| d.dev == model.devices.getId(stat.dev) else true;
|
||||
const sameino = if (e.link()) |l| l.ino == stat.ino else true;
|
||||
if (e.etype == etype and samedev and sameino) {
|
||||
_ = self.entries.removeAdapted(@as(?*model.Entry,null), HashContext{ .cmp = name });
|
||||
break :blk e;
|
||||
} else e.delStatsRec(parents);
|
||||
}
|
||||
var e = model.Entry.create(etype, main.config.extended, name);
|
||||
e.next = parents.top().sub;
|
||||
parents.top().sub = e;
|
||||
break :blk e;
|
||||
};
|
||||
// Ignore the new size/blocks field for directories, as we don't know
|
||||
// what the original values were without calling delStats() on the
|
||||
// entire subtree, which, in turn, would break all shared hardlink
|
||||
// sizes. The current approach may result in incorrect sizes after
|
||||
// refresh, but I expect the difference to be fairly minor.
|
||||
if (e.etype != .dir and (e.blocks != stat.blocks or e.size != stat.size)) {
|
||||
e.delStats(parents);
|
||||
e.blocks = stat.blocks;
|
||||
e.size = stat.size;
|
||||
}
|
||||
if (e.dir()) |d| d.dev = model.devices.getId(stat.dev);
|
||||
if (e.file()) |f| {
|
||||
f.resetFlags();
|
||||
f.notreg = !stat.dir and !stat.reg;
|
||||
}
|
||||
if (e.link()) |l| {
|
||||
l.ino = stat.ino;
|
||||
// BUG: shared sizes will be very incorrect if this is different
|
||||
// from a previous scan. May want to warn the user about that.
|
||||
l.nlink = stat.nlink;
|
||||
}
|
||||
if (e.ext()) |ext| {
|
||||
if (ext.mtime > stat.ext.mtime)
|
||||
stat.ext.mtime = ext.mtime;
|
||||
ext.* = stat.ext;
|
||||
}
|
||||
|
||||
// Assumption: l.link == 0 only happens on import, not refresh.
|
||||
if (if (e.link()) |l| l.nlink == 0 else false)
|
||||
model.link_count.add(parents.top().dev, e.link().?.ino)
|
||||
else
|
||||
e.addStats(parents);
|
||||
return e;
|
||||
}
|
||||
|
||||
fn final(self: *Self, parents: *model.Parents) void {
|
||||
if (self.entries.count() == 0) // optimization for the common case
|
||||
return;
|
||||
var it = &parents.top().sub;
|
||||
while (it.*) |e| {
|
||||
if (self.entries.contains(e)) {
|
||||
e.delStatsRec(parents);
|
||||
it.* = e.next;
|
||||
} else
|
||||
it = &e.next;
|
||||
}
|
||||
}
|
||||
|
||||
fn deinit(self: *Self) void {
|
||||
self.entries.deinit();
|
||||
}
|
||||
};
|
||||
|
||||
// Scan/import context. Entries are added in roughly the following way:
|
||||
//
|
||||
// ctx.pushPath(name)
|
||||
|
|
@ -113,6 +262,7 @@ fn writeJsonString(wr: anytype, s: []const u8) !void {
|
|||
const Context = struct {
|
||||
// When scanning to RAM
|
||||
parents: ?model.Parents = null,
|
||||
parent_entries: std.ArrayList(ScanDir) = std.ArrayList(ScanDir).init(main.allocator),
|
||||
// When scanning to a file
|
||||
wr: ?*Writer = null,
|
||||
|
||||
|
|
@ -125,6 +275,7 @@ const Context = struct {
|
|||
name: [:0]const u8 = undefined,
|
||||
|
||||
last_error: ?[:0]u8 = null,
|
||||
fatal_error: ?anyerror = null,
|
||||
|
||||
stat: Stat = undefined,
|
||||
|
||||
|
|
@ -135,7 +286,7 @@ const Context = struct {
|
|||
ui.die("Error writing to file: {s}.\n", .{ ui.errorString(e) });
|
||||
}
|
||||
|
||||
fn initFile(out: std.fs.File) Self {
|
||||
fn initFile(out: std.fs.File) *Self {
|
||||
var buf = main.allocator.create(Writer) catch unreachable;
|
||||
errdefer main.allocator.destroy(buf);
|
||||
buf.* = std.io.bufferedWriter(out.writer());
|
||||
|
|
@ -143,11 +294,17 @@ const Context = struct {
|
|||
wr.writeAll("[1,2,{\"progname\":\"ncdu\",\"progver\":\"" ++ main.program_version ++ "\",\"timestamp\":") catch |e| writeErr(e);
|
||||
wr.print("{d}", .{std.time.timestamp()}) catch |e| writeErr(e);
|
||||
wr.writeByte('}') catch |e| writeErr(e);
|
||||
return Self{ .wr = buf };
|
||||
|
||||
var self = main.allocator.create(Self) catch unreachable;
|
||||
self.* = .{ .wr = buf };
|
||||
return self;
|
||||
}
|
||||
|
||||
fn initMem() Self {
|
||||
return Self{ .parents = model.Parents{} };
|
||||
// Ownership of p is passed to the object, it will be deallocated on deinit().
|
||||
fn initMem(p: model.Parents) *Self {
|
||||
var self = main.allocator.create(Self) catch unreachable;
|
||||
self.* = .{ .parents = p };
|
||||
return self;
|
||||
}
|
||||
|
||||
fn final(self: *Self) void {
|
||||
|
|
@ -171,11 +328,15 @@ const Context = struct {
|
|||
}
|
||||
|
||||
fn popPath(self: *Self) void {
|
||||
self.path.items.len = self.path_indices.items[self.path_indices.items.len-1];
|
||||
self.path_indices.items.len -= 1;
|
||||
self.path.items.len = self.path_indices.pop();
|
||||
|
||||
if (self.stat.dir) {
|
||||
if (self.parents) |*p| if (!p.isRoot()) p.pop();
|
||||
if (self.parents) |*p| {
|
||||
var d = self.parent_entries.pop();
|
||||
d.final(p);
|
||||
d.deinit();
|
||||
if (!p.isRoot()) p.pop();
|
||||
}
|
||||
if (self.wr) |w| w.writer().writeByte(']') catch |e| writeErr(e);
|
||||
} else
|
||||
self.stat.dir = true; // repeated popPath()s mean we're closing parent dirs.
|
||||
|
|
@ -218,18 +379,9 @@ const Context = struct {
|
|||
self.last_error = main.allocator.dupeZ(u8, self.path.items) catch unreachable;
|
||||
}
|
||||
|
||||
if (self.parents) |*p| {
|
||||
var e = model.Entry.create(.file, false, self.name);
|
||||
e.insert(p);
|
||||
var f = e.file().?;
|
||||
switch (t) {
|
||||
.err => e.set_err(p),
|
||||
.other_fs => f.other_fs = true,
|
||||
.kernfs => f.kernfs = true,
|
||||
.excluded => f.excluded = true,
|
||||
}
|
||||
|
||||
} else if (self.wr) |wr|
|
||||
if (self.parents) |*p|
|
||||
self.parent_entries.items[self.parent_entries.items.len-1].addSpecial(p, self.name, t)
|
||||
else if (self.wr) |wr|
|
||||
self.writeSpecial(wr.writer(), t) catch |e| writeErr(e);
|
||||
|
||||
self.items_seen += 1;
|
||||
|
|
@ -254,25 +406,21 @@ const Context = struct {
|
|||
// Insert current path as a counted file/dir/hardlink, with information from self.stat
|
||||
fn addStat(self: *Self, dir_dev: u64) void {
|
||||
if (self.parents) |*p| {
|
||||
const etype = if (self.stat.dir) model.EType.dir
|
||||
else if (self.stat.hlinkc) model.EType.link
|
||||
else model.EType.file;
|
||||
var e = model.Entry.create(etype, main.config.extended, self.name);
|
||||
e.blocks = self.stat.blocks;
|
||||
e.size = self.stat.size;
|
||||
if (e.dir()) |d| d.dev = model.devices.getId(self.stat.dev);
|
||||
if (e.file()) |f| f.notreg = !self.stat.dir and !self.stat.reg;
|
||||
if (e.link()) |l| {
|
||||
l.ino = self.stat.ino;
|
||||
l.nlink = self.stat.nlink;
|
||||
}
|
||||
if (e.ext()) |ext| ext.* = self.stat.ext;
|
||||
var e = if (self.items_seen == 0) blk: {
|
||||
// Root entry
|
||||
var e = model.Entry.create(.dir, main.config.extended, self.name);
|
||||
e.blocks = self.stat.blocks;
|
||||
e.size = self.stat.size;
|
||||
if (e.ext()) |ext| ext.* = self.stat.ext;
|
||||
model.root = e.dir().?;
|
||||
model.root.dev = model.devices.getId(self.stat.dev);
|
||||
break :blk e;
|
||||
} else
|
||||
self.parent_entries.items[self.parent_entries.items.len-1].addStat(p, self.name, &self.stat);
|
||||
|
||||
if (self.items_seen == 0)
|
||||
model.root = e.dir().?
|
||||
else {
|
||||
e.insert(p);
|
||||
if (e.dir()) |d| p.push(d); // Enter the directory
|
||||
if (e.dir()) |d| { // Enter the directory
|
||||
if (self.items_seen != 0) p.push(d);
|
||||
self.parent_entries.append(ScanDir.init(p)) catch unreachable;
|
||||
}
|
||||
|
||||
} else if (self.wr) |wr|
|
||||
|
|
@ -287,11 +435,13 @@ const Context = struct {
|
|||
if (self.wr) |p| main.allocator.destroy(p);
|
||||
self.path.deinit();
|
||||
self.path_indices.deinit();
|
||||
self.parent_entries.deinit();
|
||||
main.allocator.destroy(self);
|
||||
}
|
||||
};
|
||||
|
||||
// Context that is currently being used for scanning.
|
||||
var active_context: ?*Context = null;
|
||||
var active_context: *Context = undefined;
|
||||
|
||||
// Read and index entries of the given dir.
|
||||
fn scanDir(ctx: *Context, dir: std.fs.Dir, dir_dev: u64) void {
|
||||
|
|
@ -378,24 +528,44 @@ fn scanDir(ctx: *Context, dir: std.fs.Dir, dir_dev: u64) void {
|
|||
}
|
||||
|
||||
pub fn scanRoot(path: []const u8, out: ?std.fs.File) !void {
|
||||
var ctx = if (out) |f| Context.initFile(f) else Context.initMem();
|
||||
active_context = &ctx;
|
||||
defer active_context = null;
|
||||
defer ctx.deinit();
|
||||
active_context = if (out) |f| Context.initFile(f) else Context.initMem(.{});
|
||||
|
||||
const full_path = std.fs.realpathAlloc(main.allocator, path) catch null;
|
||||
defer if (full_path) |p| main.allocator.free(p);
|
||||
ctx.pushPath(full_path orelse path);
|
||||
active_context.pushPath(full_path orelse path);
|
||||
|
||||
ctx.stat = try Stat.read(std.fs.cwd(), ctx.pathZ(), true);
|
||||
if (!ctx.stat.dir) return error.NotDir;
|
||||
ctx.addStat(0);
|
||||
active_context.stat = try Stat.read(std.fs.cwd(), active_context.pathZ(), true);
|
||||
if (!active_context.stat.dir) return error.NotDir;
|
||||
active_context.addStat(0);
|
||||
scan();
|
||||
}
|
||||
|
||||
var dir = try std.fs.cwd().openDirZ(ctx.pathZ(), .{ .access_sub_paths = true, .iterate = true });
|
||||
pub fn setupRefresh(parents: model.Parents) void {
|
||||
active_context = Context.initMem(parents);
|
||||
var full_path = std.ArrayList(u8).init(main.allocator);
|
||||
defer full_path.deinit();
|
||||
parents.fmtPath(true, &full_path);
|
||||
active_context.pushPath(full_path.items);
|
||||
active_context.parent_entries.append(ScanDir.init(&parents)) catch unreachable;
|
||||
active_context.stat.dir = true;
|
||||
active_context.stat.dev = model.devices.getDev(parents.top().dev);
|
||||
active_context.items_seen = 1; // The "root" item has already been added.
|
||||
}
|
||||
|
||||
// To be called after setupRefresh() (or from scanRoot())
|
||||
pub fn scan() void {
|
||||
defer active_context.deinit();
|
||||
var dir = std.fs.cwd().openDirZ(active_context.pathZ(), .{ .access_sub_paths = true, .iterate = true }) catch |e| {
|
||||
active_context.last_error = main.allocator.dupeZ(u8, active_context.path.items) catch unreachable;
|
||||
active_context.fatal_error = e;
|
||||
while (main.state == .refresh or main.state == .scan)
|
||||
main.handleEvent(true, true);
|
||||
return;
|
||||
};
|
||||
defer dir.close();
|
||||
scanDir(&ctx, dir, ctx.stat.dev);
|
||||
ctx.popPath();
|
||||
ctx.final();
|
||||
scanDir(active_context, dir, active_context.stat.dev);
|
||||
active_context.popPath();
|
||||
active_context.final();
|
||||
}
|
||||
|
||||
// Using a custom recursive descent JSON parser here. std.json is great, but
|
||||
|
|
@ -409,7 +579,7 @@ pub fn scanRoot(path: []const u8, out: ?std.fs.File) !void {
|
|||
// worth factoring out the JSON parts into a separate abstraction for which
|
||||
// tests can be written.
|
||||
const Import = struct {
|
||||
ctx: Context,
|
||||
ctx: *Context,
|
||||
|
||||
rd: std.fs.File,
|
||||
rdoff: usize = 0,
|
||||
|
|
@ -611,7 +781,7 @@ const Import = struct {
|
|||
},
|
||||
'd' => {
|
||||
if (eq(u8, key, "dsize")) {
|
||||
self.ctx.stat.blocks = @intCast(u61, self.uint(u64)>>9);
|
||||
self.ctx.stat.blocks = @intCast(model.Blocks, self.uint(u64)>>9);
|
||||
return;
|
||||
}
|
||||
if (eq(u8, key, "dev")) {
|
||||
|
|
@ -794,12 +964,8 @@ pub fn importRoot(path: [:0]const u8, out: ?std.fs.File) void {
|
|||
catch |e| ui.die("Error reading file: {s}.\n", .{ui.errorString(e)});
|
||||
defer fd.close();
|
||||
|
||||
var imp = Import{
|
||||
.ctx = if (out) |f| Context.initFile(f) else Context.initMem(),
|
||||
.rd = fd,
|
||||
};
|
||||
active_context = &imp.ctx;
|
||||
defer active_context = null;
|
||||
active_context = if (out) |f| Context.initFile(f) else Context.initMem(.{});
|
||||
var imp = Import{ .ctx = active_context, .rd = fd };
|
||||
defer imp.ctx.deinit();
|
||||
imp.root();
|
||||
imp.ctx.final();
|
||||
|
|
@ -808,9 +974,26 @@ pub fn importRoot(path: [:0]const u8, out: ?std.fs.File) void {
|
|||
var animation_pos: u32 = 0;
|
||||
var need_confirm_quit = false;
|
||||
|
||||
fn drawError(err: anyerror) void {
|
||||
const width = saturateSub(ui.cols, 5);
|
||||
const box = ui.Box.create(7, width, "Scan error");
|
||||
|
||||
box.move(2, 2);
|
||||
ui.addstr("Path: ");
|
||||
ui.addstr(ui.shorten(ui.toUtf8(active_context.last_error.?), saturateSub(width, 10)));
|
||||
|
||||
box.move(3, 2);
|
||||
ui.addstr("Error: ");
|
||||
ui.addstr(ui.shorten(ui.errorString(err), saturateSub(width, 6)));
|
||||
|
||||
box.move(5, saturateSub(width, 27));
|
||||
ui.addstr("Press any key to continue");
|
||||
}
|
||||
|
||||
fn drawBox() void {
|
||||
ui.init();
|
||||
const ctx = active_context.?;
|
||||
const ctx = active_context;
|
||||
if (ctx.fatal_error) |err| return drawError(err);
|
||||
const width = saturateSub(ui.cols, 5);
|
||||
const box = ui.Box.create(10, width, "Scanning...");
|
||||
box.move(2, 2);
|
||||
|
|
@ -878,14 +1061,14 @@ pub fn draw() void {
|
|||
.line => {
|
||||
var buf: [256]u8 = undefined;
|
||||
var line: []const u8 = undefined;
|
||||
if (active_context.?.parents == null) {
|
||||
if (active_context.parents == null) {
|
||||
line = std.fmt.bufPrint(&buf, "\x1b7\x1b[J{s: <63} {d:>9} files\x1b8",
|
||||
.{ ui.shorten(active_context.?.pathZ(), 63), active_context.?.items_seen }
|
||||
.{ ui.shorten(active_context.pathZ(), 63), active_context.items_seen }
|
||||
) catch return;
|
||||
} else {
|
||||
const r = ui.FmtSize.fmt(blocksToSize(model.root.entry.blocks));
|
||||
line = std.fmt.bufPrint(&buf, "\x1b7\x1b[J{s: <51} {d:>9} files / {s}{s}\x1b8",
|
||||
.{ ui.shorten(active_context.?.pathZ(), 51), active_context.?.items_seen, r.num(), r.unit }
|
||||
.{ ui.shorten(active_context.pathZ(), 51), active_context.items_seen, r.num(), r.unit }
|
||||
) catch return;
|
||||
}
|
||||
_ = std.io.getStdErr().write(line) catch {};
|
||||
|
|
@ -895,6 +1078,11 @@ pub fn draw() void {
|
|||
}
|
||||
|
||||
pub fn keyInput(ch: i32) void {
|
||||
if (active_context.fatal_error != null) {
|
||||
if (main.state == .scan) ui.quit()
|
||||
else main.state = .browse;
|
||||
return;
|
||||
}
|
||||
if (need_confirm_quit) {
|
||||
switch (ch) {
|
||||
'y', 'Y' => if (need_confirm_quit) ui.quit(),
|
||||
|
|
|
|||
|
|
@ -50,7 +50,7 @@ pub fn oom() void {
|
|||
|
||||
// Lazy strerror() for Zig file I/O, not complete.
|
||||
// (Would be nicer if Zig just exposed errno so I could call strerror() directly)
|
||||
pub fn errorString(e: anyerror) []const u8 {
|
||||
pub fn errorString(e: anyerror) [:0]const u8 {
|
||||
return switch (e) {
|
||||
error.DiskQuota => "Disk quota exceeded",
|
||||
error.FileTooBig => "File too big",
|
||||
|
|
|
|||
Loading…
Reference in a new issue