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 }