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.algorithm : map; 10 import std.array : empty, array; 11 import std.conv : to; 12 import std.exception : collectException; 13 import std.json : JSONValue; 14 15 import colorlog; 16 import my.named_type; 17 import my.hash; 18 import my.path : AbsolutePath, Path; 19 import my.filter : GlobFilter; 20 import my.optional; 21 22 alias FileSize = NamedType!(ulong, Tag!"FileSize", ulong.init, TagStringable, Comparable); 23 alias TimeStamp = NamedType!(long, Tag!"TimeStamp", long.init, TagStringable, Comparable); 24 25 struct OneShotFile { 26 AbsolutePath path; 27 28 /// unix time in seconds 29 TimeStamp timeStamp; 30 31 FileSize size; 32 33 bool hasChecksum; 34 35 private { 36 Checksum64 checksum_; 37 } 38 39 this(AbsolutePath path, TimeStamp timeStamp, FileSize size) { 40 this.path = path; 41 this.timeStamp = timeStamp; 42 this.size = size; 43 } 44 45 this(AbsolutePath path, TimeStamp timeStamp, FileSize size, Checksum64 cs) { 46 this.path = path; 47 this.timeStamp = timeStamp; 48 this.size = size; 49 this.checksum_ = cs; 50 this.hasChecksum = true; 51 } 52 53 Checksum64 checksum() nothrow { 54 if (hasChecksum || size.get == 0) 55 return checksum_; 56 57 checksum_ = () { 58 try { 59 return my.hash.checksum!makeChecksum64(path); 60 } catch (Exception e) { 61 logger.trace(e.msg).collectException; 62 } 63 return Checksum64.init; // an empty file 64 }(); 65 hasChecksum = true; 66 return checksum_; 67 } 68 } 69 70 struct OneShotRange { 71 import std.file : dirEntries, SpanMode, timeLastModified, getSize, isFile; 72 import std.traits : ReturnType; 73 import my.file : followSymlink, existsAnd; 74 75 private { 76 ReturnType!dirEntries entries; 77 GlobFilter gf; 78 Optional!OneShotFile front_; 79 bool followSymlink_; 80 } 81 82 this(AbsolutePath root, GlobFilter gf, bool followSymlink) nothrow { 83 try { 84 this.entries = dirEntries(root, SpanMode.depth); 85 this.gf = gf; 86 this.followSymlink_ = followSymlink; 87 } catch (Exception e) { 88 logger.trace(e.msg).collectException; 89 } 90 } 91 92 Optional!OneShotFile front() nothrow { 93 assert(!empty, "Can't get front of an empty range"); 94 95 if (front_.hasValue) 96 return front_; 97 98 () { 99 try { 100 auto f = () { 101 if (entries.front.isSymlink && followSymlink_) 102 return followSymlink(Path(entries.front.name)).orElse(Path.init).toString; 103 return entries.front.name; 104 }(); 105 106 if (f.empty) 107 return; 108 109 if (Path(f).existsAnd!isFile && gf.match(f)) { 110 front_ = OneShotFile(AbsolutePath(f), 111 f.timeLastModified.toUnixTime.TimeStamp, f.getSize.FileSize).some; 112 } 113 } catch (Exception e) { 114 logger.trace(e.msg).collectException; 115 front_ = none!OneShotFile; 116 } 117 }(); 118 119 return front_; 120 } 121 122 void popFront() @trusted nothrow { 123 assert(!empty, "Can't pop front of an empty range"); 124 125 front_ = none!OneShotFile; 126 127 try { 128 entries.popFront; 129 } catch (Exception e) { 130 logger.trace(e.msg).collectException; 131 } 132 } 133 134 bool empty() @safe nothrow { 135 try { 136 return entries.empty; 137 } catch (Exception e) { 138 logger.trace(e.msg).collectException; 139 } 140 return true; 141 } 142 } 143 144 struct FileDb { 145 OneShotFile[AbsolutePath] files; 146 string[] command; 147 148 void add(OneShotFile fc) { 149 files[fc.path] = fc; 150 } 151 152 bool isChanged(ref OneShotFile fc) { 153 if (auto v = fc.path in files) { 154 if (v.size != fc.size) 155 return true; 156 if (v.timeStamp == fc.timeStamp && v.size == fc.size) 157 return false; 158 return v.checksum != fc.checksum; 159 } 160 return true; 161 } 162 } 163 164 FileDb fromJson(string txt) nothrow { 165 import std.json : parseJSON; 166 167 FileDb rval; 168 169 auto json = () { 170 try { 171 return parseJSON(txt); 172 } catch (Exception e) { 173 logger.info(e.msg).collectException; 174 } 175 return JSONValue.init; 176 }(); 177 178 try { 179 foreach (a; json["files"].array) { 180 try { 181 rval.add(OneShotFile(AbsolutePath(a["p"].str), 182 a["t"].str.to!long.TimeStamp, 183 a["s"].str.to!ulong.FileSize, Checksum64(a["c"].str.to!ulong))); 184 } catch (Exception e) { 185 logger.trace(e.msg).collectException; 186 } 187 } 188 } catch (Exception e) { 189 logger.info(e.msg).collectException; 190 } 191 try { 192 rval.command = json["cmd"].array.map!(a => a.str).array; 193 } catch (Exception e) { 194 logger.info(e.msg).collectException; 195 } 196 197 return rval; 198 } 199 200 @("shall parse to the file database") 201 unittest { 202 auto txt = `[{"p": "foo/bar", "c": "1234", "t": "42"}]`; 203 auto db = fromJson(txt); 204 assert(AbsolutePath("foo/bar") in db.files); 205 assert(db.files[AbsolutePath("foo/bar")].checksum == Checksum64(1234)); 206 } 207 208 JSONValue toJson(ref FileDb db) { 209 import std.array : appender; 210 import std.path : relativePath; 211 212 auto app = appender!(JSONValue[])(); 213 foreach (fc; db.files.byValue) { 214 try { 215 JSONValue v; 216 v["p"] = relativePath(fc.path.toString); 217 v["c"] = fc.hasChecksum ? fc.checksum.c0.to!string : "0"; 218 v["t"] = fc.timeStamp.get.to!string; 219 v["s"] = fc.size.get.to!string; 220 app.put(v); 221 } catch (Exception e) { 222 } 223 } 224 JSONValue rval; 225 rval["files"] = app.data; 226 rval["cmd"] = db.command; 227 228 return rval; 229 }