1 /** 2 Copyright: Copyright (c) 2021, 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 watchexec_internal.oneshot; 7 8 import logger = std.experimental.logger; 9 import std.conv : to; 10 import std.exception : collectException; 11 import std.json : JSONValue; 12 13 import colorlog; 14 import my.named_type; 15 import my.hash; 16 import my.path : AbsolutePath; 17 import my.filter : GlobFilter; 18 import my.optional; 19 20 alias FileSize = NamedType!(ulong, Tag!"FileSize", ulong.init, TagStringable, Comparable); 21 alias TimeStamp = NamedType!(long, Tag!"TimeStamp", long.init, TagStringable, Comparable); 22 23 struct OneShotFile { 24 AbsolutePath path; 25 26 /// unix time in seconds 27 TimeStamp timeStamp; 28 29 FileSize size; 30 31 bool hasChecksum; 32 33 private { 34 Checksum64 checksum_; 35 } 36 37 this(AbsolutePath path, TimeStamp timeStamp, FileSize size) { 38 this.path = path; 39 this.timeStamp = timeStamp; 40 this.size = size; 41 } 42 43 this(AbsolutePath path, TimeStamp timeStamp, FileSize size, Checksum64 cs) { 44 this.path = path; 45 this.timeStamp = timeStamp; 46 this.size = size; 47 this.checksum_ = cs; 48 this.hasChecksum = true; 49 } 50 51 Checksum64 checksum() nothrow { 52 if (hasChecksum) 53 return checksum_; 54 55 checksum_ = () { 56 try { 57 return my.hash.checksum!makeChecksum64(path); 58 } catch (Exception e) { 59 logger.trace(e.msg).collectException; 60 } 61 return Checksum64.init; // an empty file 62 }(); 63 hasChecksum = true; 64 return checksum_; 65 } 66 } 67 68 struct OneShotRange { 69 import std.file : dirEntries, SpanMode; 70 import std.traits : ReturnType; 71 72 private { 73 ReturnType!dirEntries entries; 74 GlobFilter gf; 75 } 76 77 this(AbsolutePath root, GlobFilter gf) { 78 this.entries = dirEntries(root, SpanMode.depth); 79 this.gf = gf; 80 } 81 82 Optional!OneShotFile front() { 83 assert(!empty, "Can't get front of an empty range"); 84 auto f = entries.front; 85 if (f.isFile && gf.match(f.name)) { 86 return OneShotFile(AbsolutePath(f.name), 87 f.timeLastModified.toUnixTime.TimeStamp, f.size.FileSize).some; 88 } 89 return none!OneShotFile; 90 } 91 92 void popFront() @safe { 93 assert(!empty, "Can't pop front of an empty range"); 94 entries.popFront; 95 } 96 97 bool empty() @safe { 98 return entries.empty; 99 } 100 } 101 102 struct FileDb { 103 OneShotFile[AbsolutePath] files; 104 105 void add(OneShotFile fc) { 106 files[fc.path] = fc; 107 } 108 109 bool isChanged(ref OneShotFile fc) { 110 if (auto v = fc.path in files) { 111 if (v.timeStamp == fc.timeStamp && v.size == fc.size) 112 return false; 113 return v.checksum != fc.checksum; 114 } 115 return true; 116 } 117 } 118 119 FileDb fromJson(string txt) nothrow { 120 import std.json : parseJSON; 121 122 FileDb rval; 123 124 try { 125 foreach (a; parseJSON(txt).array) { 126 try { 127 rval.add(OneShotFile(AbsolutePath(a["p"].str), 128 a["t"].str.to!long.TimeStamp, 129 a["s"].str.to!ulong.FileSize, Checksum64(a["c"].str.to!ulong))); 130 } catch (Exception e) { 131 logger.trace(e.msg).collectException; 132 } 133 } 134 } catch (Exception e) { 135 logger.info(e.msg).collectException; 136 } 137 138 return rval; 139 } 140 141 @("shall parse to the file database") 142 unittest { 143 auto txt = `[{"p": "foo/bar", "c": "1234", "t": "42"}]`; 144 auto db = fromJson(txt); 145 assert(AbsolutePath("foo/bar") in db.files); 146 assert(db.files[AbsolutePath("foo/bar")].checksum == Checksum64(1234)); 147 } 148 149 JSONValue toJson(ref FileDb db) { 150 import std.array : appender; 151 import std.path : relativePath; 152 153 auto app = appender!(JSONValue[])(); 154 JSONValue rval; 155 foreach (fc; db.files.byValue) { 156 try { 157 JSONValue v; 158 v["p"] = relativePath(fc.path.toString); 159 v["c"] = fc.hasChecksum ? fc.checksum.c0.to!string : "0"; 160 v["t"] = fc.timeStamp.get.to!string; 161 v["s"] = fc.size.get.to!string; 162 app.put(v); 163 } catch (Exception e) { 164 } 165 } 166 167 return JSONValue(app.data); 168 }