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 app; 7 8 import logger = std.experimental.logger; 9 import std.algorithm : remove; 10 import std.exception : collectException; 11 import std.path; 12 import std.stdio : writeln; 13 import std..string; 14 15 import colorlog; 16 17 import dsnapshot.config; 18 19 int main(string[] args) { 20 import dsnapshot.cmdgroup.backup; 21 import dsnapshot.cmdgroup.diskusage; 22 import dsnapshot.cmdgroup.remote; 23 import dsnapshot.cmdgroup.restore; 24 25 confLogger(VerboseMode.info); 26 27 auto conf = parseUserArgs(args); 28 29 confLogger(conf.global.verbosity); 30 31 logger.trace(conf.global); 32 static foreach (T; Config.Type.AllowedTypes) { 33 if (auto a = conf.data.peek!T) 34 logger.tracef("%s", *a); 35 } 36 37 if (conf.global.help) { 38 return cmdHelp(conf); 39 } 40 41 import std.variant : visit; 42 43 // dfmt off 44 return conf.data.visit!( 45 (Config.Help a) => cmdHelp(conf), 46 (Config.Backup a) { 47 loadConfig(conf); 48 return cmdBackup(conf.global, a, conf.snapshots); 49 }, 50 (Config.Remotecmd a) => cmdRemote(a), 51 (Config.Diskusage a) { 52 loadConfig(conf); 53 return cmdDiskUsage(conf.snapshots, a); 54 }, 55 (Config.Restore a) { 56 loadConfig(conf); 57 return cmdRestore(conf.snapshots, a); 58 }, 59 (Config.Verifyconfig a) { 60 loadConfig(conf); 61 logger.info("Done"); 62 return 0; 63 } 64 ); 65 // dfmt on 66 } 67 68 @safe: 69 private: 70 71 int cmdHelp(Config conf) @trusted { 72 conf.printHelp; 73 return 0; 74 } 75 76 Config parseUserArgs(string[] args) @trusted { 77 import std.format : format; 78 import std..string : toLower; 79 import std.traits : EnumMembers; 80 static import std.getopt; 81 82 Config conf; 83 conf.data = Config.Help.init; 84 conf.global.progName = args[0].baseName; 85 conf.global.confFile = ".dsnapshot.toml"; 86 87 string group; 88 if (args.length > 1) { 89 group = args[1]; 90 args = args.remove(1); 91 } 92 93 try { 94 string confFile; 95 96 void globalParse() { 97 // dfmt off 98 conf.global.helpInfo = std.getopt.getopt(args, std.getopt.config.passThrough, 99 "v|verbose", format("Set the verbosity (%-(%s, %))", [EnumMembers!(VerboseMode)]), &conf.global.verbosity, 100 "c|config", "Config file to read", &confFile, 101 ); 102 // dfmt on 103 } 104 105 void backupParse() { 106 Config.Backup data; 107 scope (success) 108 conf.data = data; 109 110 // dfmt off 111 data.helpInfo = std.getopt.getopt(args, 112 "ignore-rsync-error-code", "Ignore rsync error code", &data.ignoreRsyncErrorCodes, 113 "s|snapshot", "The name of the snapshot to backup (default: all)", &data.name.value, 114 ); 115 // dfmt on 116 } 117 118 void remotecmdParse() { 119 Config.Remotecmd data; 120 scope (success) 121 conf.data = data; 122 123 // dfmt off 124 data.helpInfo = std.getopt.getopt(args, 125 "cmd", format("Command to execute (%-(%s, %))", [EnumMembers!(RemoteSubCmd)]), &data.cmd, 126 "path", "Path argument for the command", &data.path, 127 ); 128 // dfmt on 129 } 130 131 void diskusageParse() { 132 Config.Diskusage data; 133 scope (success) 134 conf.data = data; 135 136 // dfmt off 137 data.helpInfo = std.getopt.getopt(args, 138 "s|snapshot", "Name of the snapshot to calculate the disk usage for", &data.name.value, 139 ); 140 // dfmt on 141 } 142 143 void restoreParse() { 144 import std.datetime : SysTime, UTC, Clock; 145 146 Config.Restore data; 147 scope (success) 148 conf.data = data; 149 150 string time; 151 // dfmt off 152 data.helpInfo = std.getopt.getopt(args, 153 "delete", "Delete files from dst if they are removed in the snapshot", &data.deleteFromTo, 154 "dst", "Where to restore the snapshot", &data.restoreTo, 155 "s|snapshot", "Name of the snapshot to calculate the disk usage for", &data.name.value, 156 "time", "Pick the snapshot that is closest to this time (default: now). Add a trailing Z if UTC", &time, 157 ); 158 // dfmt on 159 160 try { 161 if (time.length == 0) 162 data.time = Clock.currTime; 163 else 164 data.time = SysTime.fromISOExtString(time); 165 } catch (Exception e) { 166 logger.error(e.msg); 167 throw new Exception("Example of UTC time: 2019-07-18T10:49:29.5765454Z"); 168 } 169 } 170 171 void verifyconfigParse() { 172 conf.data = Config.Verifyconfig.init; 173 } 174 175 alias ParseFn = void delegate(); 176 ParseFn[string] parsers; 177 178 if (confFile.length != 0) 179 conf.global.confFile = confFile.Path; 180 181 globalParse; 182 183 static foreach (T; Config.Type.AllowedTypes) { 184 static if (!is(T == Config.Help)) 185 mixin(format(`parsers["%1$s"] = &%1$sParse;`, T.stringof.toLower)); 186 } 187 188 conf.global.help = true; 189 if (auto p = group in parsers) { 190 (*p)(); 191 conf.global.help = conf.global.helpInfo.helpWanted; 192 } 193 } catch (std.getopt.GetOptException e) { 194 // unknown option 195 conf.global.help = true; 196 logger.error(e.msg); 197 } catch (Exception e) { 198 conf.global.help = true; 199 logger.error(e.msg); 200 } 201 202 return conf; 203 } 204 205 void loadConfig(ref Config conf) @trusted { 206 import std.algorithm : filter, map; 207 import std.array : array; 208 import std.conv : to; 209 import std.file : exists, readText, isFile; 210 import std.path : dirName, buildPath; 211 import toml; 212 213 scope (exit) 214 logger.trace(conf); 215 216 if (!exists(conf.global.confFile.toString) || !conf.global.confFile.toString.isFile) { 217 logger.errorf("Configuration %s do not exist", conf.global.confFile); 218 return; 219 } 220 221 static auto tryLoading(string configFile) { 222 auto txt = readText(configFile); 223 auto doc = parseTOML(txt); 224 return doc; 225 } 226 227 TOMLDocument doc; 228 try { 229 doc = tryLoading(conf.global.confFile.toString); 230 } catch (Exception e) { 231 logger.warning("Unable to read the configuration from ", conf.global.confFile); 232 logger.warning(e.msg); 233 return; 234 } 235 236 alias Fn = void delegate(ref Config c, ref TOMLValue v); 237 Fn[string] tables; 238 239 tables["snapshot"] = (ref Config c, ref TOMLValue snapshots) { 240 foreach (name, data; snapshots) { 241 Snapshot s; 242 s.name = name; 243 s.layout = makeDefaultLayout; 244 foreach (k, v; data) { 245 try { 246 switch (k) { 247 case "rsync": 248 auto rsync = parseRsync(v, name); 249 s.syncCmd = rsync; 250 break; 251 case "pre_exec": 252 s.hooks.preExec = v.array.map!(a => a.str).array; 253 break; 254 case "post_exec": 255 s.hooks.postExec = v.array.map!(a => a.str).array; 256 break; 257 case "span": 258 auto layout = parseLayout(v); 259 s.layout = layout; 260 break; 261 case "dsnapshot": 262 s.remoteCmd = s.remoteCmd.match!((SshRemoteCmd a) { 263 a.dsnapshot = v.str; 264 return a; 265 }); 266 break; 267 case "rsh": 268 s.remoteCmd = s.remoteCmd.match!((SshRemoteCmd a) { 269 a.rsh = v.array.map!(a => a.str).array; 270 return a; 271 }); 272 break; 273 default: 274 logger.infof("Unknown option '%s' in section 'snapshot.%s' in configuration", 275 k, name); 276 } 277 } catch (Exception e) { 278 logger.error(e.msg).collectException; 279 } 280 } 281 c.snapshots ~= s; 282 } 283 }; 284 285 foreach (curr; doc.byKeyValue.filter!(a => a.value.type == TOML_TYPE.TABLE)) { 286 try { 287 if (auto t = curr.key in tables) { 288 (*t)(conf, curr.value); 289 } else { 290 logger.infof("Unknown section '%s' in configuration", curr.key); 291 } 292 } catch (Exception e) { 293 logger.error(e.msg).collectException; 294 } 295 } 296 } 297 298 import toml : TOMLValue; 299 300 auto parseLayout(ref TOMLValue tv) @trusted { 301 import std.algorithm : sort, map; 302 import std.array : array; 303 import std.conv : to; 304 import std.datetime : Duration, dur, Clock; 305 import std.range : chunks; 306 import std..string : split; 307 import std.typecons : Nullable; 308 import dsnapshot.layout : Span, LayoutConfig, Layout; 309 310 Span[long] spans; 311 312 static Nullable!Span parseASpan(ref TOMLValue data) { 313 typeof(return) rval; 314 315 immutable intervalKey = "interval"; 316 immutable nrKey = "nr"; 317 if (intervalKey !in data || nrKey !in data) { 318 logger.warning("Missing either 'nr' or 'space' key"); 319 return rval; 320 } 321 322 const parts = data[intervalKey].str.split; 323 if (parts.length % 2 != 0) { 324 logger.warning( 325 "Invalid space specification because either the number or unit is missing"); 326 return rval; 327 } 328 329 Duration d; 330 foreach (const p; parts.chunks(2)) { 331 const nr = p[0].to!long; 332 bool validUnit; 333 immutable AllUnites = [ 334 "msecs", "seconds", "minutes", "hours", "days", "weeks" 335 ]; 336 static foreach (Unit; AllUnites) { 337 if (p[1] == Unit) { 338 d += nr.dur!Unit; 339 validUnit = true; 340 } 341 } 342 if (!validUnit) { 343 logger.warningf("Invalid unit '%s'. Valid are %-(%s, %).", p[1], AllUnites); 344 return rval; 345 } 346 } 347 348 const nr = data[nrKey].integer; 349 if (nr <= 0) { 350 logger.warning("nr must be positive"); 351 return rval; 352 } 353 354 rval = Span(cast(uint) nr, d); 355 356 return rval; 357 } 358 359 foreach (key, data; tv) { 360 try { 361 const idx = key.to!long; 362 auto span = parseASpan(data); 363 if (span.isNull) { 364 logger.warning(tv); 365 } else { 366 spans[idx] = span.get; 367 } 368 } catch (Exception e) { 369 logger.warning(e.msg); 370 logger.warning(tv); 371 } 372 } 373 374 return Layout(Clock.currTime, LayoutConfig(spans.byKeyValue 375 .array 376 .sort!((a, b) => a.key < b.key) 377 .map!(a => a.value) 378 .array)); 379 } 380 381 auto parseRsync(ref TOMLValue tv, const string parent) @trusted { 382 import std.algorithm : map; 383 import std.array : array, empty; 384 import std.format : format; 385 import dsnapshot.types; 386 387 RsyncConfig rval; 388 389 string src, srcAddr, dst, dstAddr; 390 foreach (key, data; tv) { 391 switch (key) { 392 case "src": 393 src = data.str; 394 break; 395 case "src_addr": 396 srcAddr = data.str; 397 break; 398 case "dst": 399 dst = data.str; 400 break; 401 case "dst_addr": 402 dstAddr = data.str; 403 break; 404 case "cmd_rsync": 405 rval.cmdRsync = data.str; 406 break; 407 case "cross_fs": 408 rval.oneFs = data == true; 409 break; 410 case "fakeroot": 411 rval.useFakeRoot = data == true; 412 break; 413 case "link_dest": 414 rval.useLinkDest = data == true; 415 break; 416 case "low_prio": 417 rval.lowPrio = data == true; 418 break; 419 case "exclude": 420 rval.exclude = data.array.map!(a => a.str).array; 421 break; 422 case "rsync_cmd": 423 rval.cmdRsync = data.str; 424 break; 425 case "diskusage_cmd": 426 rval.cmdDiskUsage = data.array.map!(a => a.str).array; 427 break; 428 case "rsync_args": 429 rval.args = data.array.map!(a => a.str).array; 430 break; 431 case "progress": 432 rval.progress = data.array.map!(a => a.str).array; 433 break; 434 default: 435 logger.infof("Unknown option '%s' in section 'snapshot.%s.rsync' in configuration", 436 key, parent); 437 } 438 } 439 440 if (srcAddr.empty && dstAddr.empty) { 441 rval.flow = FlowLocal(LocalAddr(src.expandTilde), LocalAddr(dst)); 442 } else if (!srcAddr.empty && dstAddr.empty) { 443 rval.flow = FlowRsyncToLocal(RsyncAddr(srcAddr, src), LocalAddr(dst)); 444 } else if (srcAddr.empty && !dstAddr.empty) { 445 rval.flow = FlowLocalToRsync(LocalAddr(src), RsyncAddr(dstAddr, dst)); 446 } else { 447 logger.warning("The combination of src, src_addr, dst and dst_addr is not supported. It either has to be local->local, local->remote, remote->local"); 448 } 449 450 return rval; 451 } 452 453 /** The default layout to use if none is specified by the user. 454 */ 455 auto makeDefaultLayout() { 456 import std.datetime : Clock, dur; 457 import dsnapshot.layout; 458 459 const base = Clock.currTime; 460 auto conf = LayoutConfig([ 461 Span(6, 4.dur!"hours"), Span(6, 1.dur!"days"), Span(3, 1.dur!"weeks") 462 ]); 463 return Layout(base, conf); 464 }