1 /** 2 Copyright: Copyright (c) 2019, Joakim Brännström. All rights reserved. 3 License: $(LINK2 http://www.boost.org/LICENSE_1_0.txt, Boost Software License 1.0) 4 Author: Joakim Brännström (joakim.brannstrom@gmx.com) 5 */ 6 module dsnapshot.layout_utils; 7 8 import logger = std.experimental.logger; 9 import std.array : empty; 10 import std.exception : collectException; 11 12 import sumtype; 13 14 import dsnapshot.exception; 15 import dsnapshot.layout : Name, Layout; 16 import dsnapshot.types; 17 18 version (unittest) { 19 import unit_threaded.assertions; 20 } 21 22 @safe: 23 24 Layout fillLayout(const Layout layout_, const Flow flow, const RemoteCmd cmd) { 25 import std.algorithm : filter, map, sort; 26 import std.array : array; 27 import std.conv : to; 28 import std.datetime : UTC, DateTimeException, SysTime; 29 import std.file : dirEntries, SpanMode, exists, isDir; 30 import std.path : baseName; 31 import dsnapshot.layout : LSnapshot = Snapshot; 32 33 Layout rval = layout_.dup; 34 35 const names = flow.match!((None a) => null, 36 (FlowRsyncToLocal a) => snapshotNamesFromDir(a.dst.value.Path), 37 (FlowLocal a) => snapshotNamesFromDir(a.dst.value.Path), 38 (FlowLocalToRsync a) => snapshotNamesFromSsh(cmd, a.dst.addr, a.dst.path)); 39 40 foreach (const n; names) { 41 try { 42 const t = SysTime.fromISOExtString(n.value, UTC()); 43 rval.put(LSnapshot(t, n)); 44 } catch (DateTimeException e) { 45 logger.warning("Unable to extract the time from the snapshot name"); 46 logger.info(e.msg); 47 logger.info("It is added as a snapshot taken at the time ", SysTime.min); 48 rval.put(LSnapshot(SysTime.min, n)); 49 } 50 } 51 52 rval.finalize; 53 54 return rval; 55 } 56 57 Name[] snapshotNamesFromDir(Path dir) @trusted { 58 import std.algorithm : filter, map, copy; 59 import std.array : appender; 60 import std.file : dirEntries, SpanMode, exists, isDir; 61 import std.path : baseName; 62 63 if (!dir.toString.exists) 64 return null; 65 if (!dir.toString.isDir) 66 throw new SnapshotException(SnapshotException.DstIsNotADir.init.SnapshotError); 67 68 auto app = appender!(Name[])(); 69 dirEntries(dir.toString, SpanMode.shallow).map!(a => a.name) 70 .filter!(a => a.isDir) 71 .map!(a => Name(a.baseName)) 72 .copy(app); 73 return app.data; 74 } 75 76 Name[] snapshotNamesFromSsh(const RemoteCmd cmd_, string addr, string path) { 77 import std.algorithm : map, copy; 78 import std.array : appender; 79 import std.process : execute; 80 import std.string : splitLines; 81 82 auto cmd = cmd_.match!((const SshRemoteCmd a) { 83 return a.toCmd(RemoteSubCmd.lsDirs, addr, path); 84 }); 85 if (cmd.empty) 86 return null; 87 88 auto res = execute(cmd); 89 if (res.status != 0) { 90 logger.warning(res.output); 91 return null; 92 } 93 94 auto app = appender!(Name[])(); 95 res.output.splitLines.map!(a => Name(a)).copy(app); 96 97 return app.data; 98 } 99 100 @("shall scan the directory for all snapshots") 101 unittest { 102 import std.conv : to; 103 import std.datetime : SysTime, UTC, Duration, Clock, dur, Interval; 104 import std.file; 105 import std.path; 106 import std.range : enumerate; 107 import sumtype; 108 import dsnapshot.layout : Layout, LayoutConfig, Span; 109 110 immutable tmpDir = "test_snapshot_scan"; 111 scope (exit) 112 () @trusted { rmdirRecurse(tmpDir); }(); 113 mkdir(tmpDir).collectException; 114 115 const offset = 5.dur!"minutes"; 116 const base = Clock.currTime.toUTC; 117 auto curr = cast() base; 118 119 foreach (const i; 0 .. 15) { 120 mkdir(buildPath(tmpDir, curr.toISOExtString)); 121 curr -= 4.dur!"hours"; 122 } 123 foreach (const i; 0 .. 4) { 124 curr -= 8.dur!"hours"; 125 mkdir(buildPath(tmpDir, curr.toISOExtString)); 126 } 127 128 auto conf = LayoutConfig([Span(5, 4.dur!"hours"), Span(5, 1.dur!"days")]); 129 auto layout = Layout(base, conf); 130 layout = fillLayout(layout, FlowLocal(LocalAddr(tmpDir), LocalAddr(tmpDir)) 131 .Flow, RemoteCmd(SshRemoteCmd.init)); 132 133 layout.discarded.length.shouldEqual(10); 134 135 (base - layout.snapshotTimeInBucket(0).get).total!"minutes".shouldEqual(0); 136 (base - layout.snapshotTimeInBucket(4).get).total!"hours".shouldEqual(3 * 5 + 1); 137 (base - layout.snapshotTimeInBucket(5).get).total!"hours".shouldEqual(4 * 5); 138 (base - layout.snapshotTimeInBucket(6).get).total!"hours".shouldEqual(4 * 5 + 24 * 1); 139 (base - layout.snapshotTimeInBucket(7).get).total!"hours".shouldEqual(4 * 5 + 24 * 2); 140 /// these buckets are filled by the second pass 141 (base - layout.snapshotTimeInBucket(8).get).total!"hours".shouldEqual(4 * 5 + 24 * 3); 142 layout.snapshotTimeInBucket(9).isNull.shouldBeTrue; 143 }