CH-J ToolsSerververwaltung über SSH
Menü
Veröffentlichte Quellen

CH-J Tools

Durchsuchen Sie Verzeichnisse und Dateien einer bestimmten Anwendungsausgabe.

Quellen als ZIP herunterladen
CH-J / Server Manager 1704 Dateien
0.1.2-beta Hinzugefügt: · Erstellt:
parse.rs Hinzugefügt: Datum nicht erfasst 10,8 KB · 267 Zeilen
Code Datei herunterladen
1//! Reads what rsync prints: one line per change, the overall progress and the closing statistics.
3use once_cell::sync::Lazy;
4use regex::Regex;
5use serde_json::{json, Value};
7#[derive(Debug, PartialEq, Clone)]
8pub enum Change {
9 New,
10 Changed,
11 MetadataOnly,
12 Delete,
13 Directory,
14 Link,
17impl Change {
18 pub fn name(&self) -> &'static str {
19 match self {
20 Change::New => "new",
21 Change::Changed => "changed",
22 Change::MetadataOnly => "metadata",
23 Change::Delete => "delete",
24 Change::Directory => "directory",
25 Change::Link => "link",
26 }
27 }
30#[derive(Debug, PartialEq, Clone)]
31pub struct Item {
32 pub change: Change,
33 pub size: u64,
34 pub path: String,
35 pub flags: String,
38/// rsync writes unusual bytes of a name as `\#ooo`.
39pub fn unescape(name: &str) -> String {
40 static ESCAPE: Lazy<Regex> = Lazy::new(|| Regex::new(r"\\#([0-7]{3})").unwrap());
41 if !name.contains("\\#") {
42 return name.to_string();
43 }
44 let mut bytes = Vec::with_capacity(name.len());
45 let mut last = 0;
46 for found in ESCAPE.captures_iter(name) {
47 let whole = found.get(0).unwrap();
48 bytes.extend_from_slice(&name.as_bytes()[last..whole.start()]);
49 bytes.push(u8::from_str_radix(&found[1], 8).unwrap_or(b'?'));
50 last = whole.end();
51 }
52 bytes.extend_from_slice(&name.as_bytes()[last..]);
53 String::from_utf8_lossy(&bytes).into_owned()
56fn classify(flags: &str) -> Option<Change> {
57 let bytes = flags.as_bytes();
58 if bytes.len() < 2 {
59 return None;
60 }
61 if flags.starts_with("*deleting") {
62 return Some(Change::Delete);
63 }
64 if !matches!(bytes[0], b'<' | b'>' | b'c' | b'h' | b'.' | b'*') {
65 return None;
66 }
67 let attributes = &flags[2..];
68 let created = attributes.contains('+');
69 Some(match bytes[1] {
70 b'd' if created => Change::Directory,
71 b'd' => Change::MetadataOnly,
72 b'L' | b'D' | b'S' => Change::Link,
73 _ if created => Change::New,
74 _ if bytes[0] == b'.' => Change::MetadataOnly,
75 _ => Change::Changed,
76 })
79/// One line of `--out-format=%i<TAB>%l<TAB>%n`, or a plain `*deleting name` line.
80pub fn item(line: &str) -> Option<Item> {
81 let mut parts = line.splitn(3, '\t');
82 if let (Some(flags), Some(size), Some(name)) = (parts.next(), parts.next(), parts.next()) {
83 let flags = flags.trim();
84 let change = classify(flags)?;
85 return Some(Item { change, size: size.trim().parse().unwrap_or(0), path: unescape(name), flags: flags.to_string() });
86 }
87 let rest = line.strip_prefix("*deleting")?;
88 // openrsync separates with exactly one space; any other belongs to the name.
89 Some(Item { change: Change::Delete, size: 0, path: unescape(rest.strip_prefix(' ').unwrap_or(rest)), flags: "*deleting".into() })
92/// ` 1,234,567 45% 1.20MB/s 0:00:12 (xfr#3, to-chk=1/4)`.
93pub fn progress(line: &str) -> Option<Value> {
94 static LINE: Lazy<Regex> = Lazy::new(|| Regex::new(r"^\s*([\d,.]+)([KMGT]?)\s+(\d{1,3})%\s+([\d.,]+\S*/s)\s+(\S+)").unwrap());
95 let found = LINE.captures(line)?;
96 let bytes: u64 = found[1].replace([',', '.'], "").parse().ok()?;
97 Some(json!({ "bytes": bytes, "percent": found[3].parse::<u32>().ok()?.min(100), "rate": &found[4], "remaining": &found[5] }))
100/// `1,234 bytes`, or the rounded `1235 kB` of openrsync.
101fn count(text: &str) -> u64 {
102 let mut words = text.split_whitespace();
103 let number: u64 = words.next().unwrap_or("").replace([',', '.'], "").parse().unwrap_or(0);
104 number.saturating_mul(match words.next() {
105 Some("kB" | "KB") => 1000,
106 Some("MB") => 1000 * 1000,
107 Some("GB") => 1000 * 1000 * 1000,
108 Some("TB") => 1000 * 1000 * 1000 * 1000,
109 _ => 1,
110 })
113/// The closing block of `--stats`. The wording differs a little between rsync and openrsync.
114pub fn statistics(lines: &[String]) -> Value {
115 let mut stats = serde_json::Map::new();
116 let names = [
117 ("Number of files:", "files"),
118 ("Number of created files:", "created"),
119 ("Number of deleted files:", "deleted"),
120 ("Number of regular files transferred:", "transferred"),
121 ("Number of files transferred:", "transferred"),
122 ("Total file size:", "totalBytes"),
123 ("Total transferred file size:", "transferredBytes"),
124 ("Literal data:", "literalBytes"),
125 ("Unmatched data:", "literalBytes"),
126 ("Matched data:", "matchedBytes"),
127 ("Total bytes sent:", "sentBytes"),
128 ("Total bytes received:", "receivedBytes"),
129 ("Total sent:", "sentBytes"),
130 ("Total received:", "receivedBytes"),
131 ];
132 for line in lines {
133 for (prefix, name) in names {
134 if let Some(rest) = line.trim().strip_prefix(prefix) {
135 stats.insert(name.to_string(), json!(count(rest)));
136 }
137 }
138 }
139 Value::Object(stats)
142#[derive(Default)]
143pub struct Summary {
144 pub new: u64,
145 pub changed: u64,
146 pub metadata: u64,
147 pub delete: u64,
148 pub directories: u64,
149 pub links: u64,
150 pub bytes: u64,
153impl Summary {
154 pub fn add(&mut self, item: &Item) {
155 match item.change {
156 Change::New => {
157 self.new += 1;
158 self.bytes += item.size;
159 }
160 Change::Changed => {
161 self.changed += 1;
162 self.bytes += item.size;
163 }
164 Change::MetadataOnly => self.metadata += 1,
165 Change::Delete => self.delete += 1,
166 Change::Directory => self.directories += 1,
167 Change::Link => self.links += 1,
168 }
169 }
171 pub fn to_json(&self) -> Value {
172 json!({ "new": self.new, "changed": self.changed, "metadata": self.metadata, "delete": self.delete, "directories": self.directories, "links": self.links, "bytes": self.bytes })
173 }
176/// What an exit code of rsync means for the user. Code 24 means files vanished while reading, normal on a live server.
177pub fn exit_meaning(code: i64) -> (&'static str, &'static str) {
178 match code {
179 0 => ("ok", "Finished."),
180 24 => ("warning", "Finished, but some source files disappeared during the transfer."),
181 23 => ("partial", "Finished only in part: some files could not be transferred. See the output."),
182 25 => ("stopped", "Stopped: more files were to be deleted than the preview showed. Run the preview again."),
183 30 | 35 => ("failed", "The connection timed out."),
184 12 | 13 | 255 => ("failed", "The connection or the rsync protocol failed. See the output."),
185 20 => ("cancelled", "Cancelled."),
186 127 => ("failed", "rsync was not found."),
187 1 => ("failed", "rsync refused an option or a path. See the output."),
188 3 | 11 => ("failed", "A file or a directory could not be accessed. See the output."),
189 _ => ("failed", "rsync failed. See the output."),
190 }
193#[cfg(test)]
194mod tests {
195 use super::*;
197 #[test]
198 fn itemized_lines_become_changes() {
199 for (line, change, size, path) in [
200 (">f+++++++++\t120\tnew.txt", Change::New, 120, "new.txt"),
201 ("<f+++++++++\t7\tsub/new.txt", Change::New, 7, "sub/new.txt"),
202 (">f.st......\t99\tchanged.bin", Change::Changed, 99, "changed.bin"),
203 (">fc.t......\t5\tchecksum.txt", Change::Changed, 5, "checksum.txt"),
204 (".f...p.....\t5\tmode.txt", Change::MetadataOnly, 5, "mode.txt"),
205 ("cd+++++++++\t4096\tdir/", Change::Directory, 4096, "dir/"),
206 (".d..t......\t4096\t./", Change::MetadataOnly, 4096, "./"),
207 ("cL+++++++++\t11\tlink -> target", Change::Link, 11, "link -> target"),
208 ("hf+++++++++\t3\thard", Change::New, 3, "hard"),
209 ("*deleting \t0\told.txt", Change::Delete, 0, "old.txt"),
210 ("*deleting plain name.txt", Change::Delete, 0, "plain name.txt"),
211 ("*deleting leading space", Change::Delete, 0, " leading space"),
212 ] {
213 let parsed = item(line).unwrap_or_else(|| panic!("{line}"));
214 assert_eq!((parsed.change, parsed.size, parsed.path.as_str()), (change, size, path), "{line}");
215 }
216 }
218 #[test]
219 fn other_output_is_not_a_change() {
220 for line in ["", "sending incremental file list", "Number of files: 3", "rsync error: some files could not be transferred (code 23)", "sent 1 bytes received 2 bytes", " 1,234 45% 1.20MB/s 0:00:12"] {
221 assert_eq!(item(line), None, "{line}");
222 }
223 }
225 #[test]
226 fn names_keep_tabs_and_unusual_bytes() {
227 assert_eq!(item(">f+++++++++\t1\ta\tb.txt").unwrap().path, "a\tb.txt");
228 assert_eq!(item(">f+++++++++\t1\tline\\#012break").unwrap().path, "line\nbreak");
229 assert_eq!(unescape("p\\#305\\#231\\#303\\#255"), "p\u{159}\u{ed}");
230 assert_eq!(unescape("plain\\name"), "plain\\name");
231 }
233 #[test]
234 fn overall_progress_is_read() {
235 let value = progress(" 32,768 45% 1.20MB/s 0:00:12 (xfr#3, to-chk=1/4)").unwrap();
236 assert_eq!(value["bytes"], 32768);
237 assert_eq!(value["percent"], 45);
238 assert_eq!(value["rate"], "1.20MB/s");
239 assert_eq!(progress("sending incremental file list"), None);
240 }
242 #[test]
243 fn statistics_of_both_implementations_are_read() {
244 let stats = statistics(&["Number of files: 1,234 (reg: 1,000, dir: 234)", "Number of created files: 3", "Number of deleted files: 2", "Number of regular files transferred: 5", "Total file size: 9,876,543 bytes", "Total transferred file size: 1,000 bytes", "Literal data: 800 bytes", "Matched data: 200 bytes"].map(String::from));
245 assert_eq!(stats, json!({ "files": 1234, "created": 3, "deleted": 2, "transferred": 5, "totalBytes": 9876543, "transferredBytes": 1000, "literalBytes": 800, "matchedBytes": 200 }));
246 let open = statistics(&["Number of files: 2", "Number of files transferred: 1", "Total file size: 1235 kB", "Unmatched data: 4 B", "Matched data: 2 MB", "Total sent: 8 B", "Total received: 9 B"].map(String::from));
247 assert_eq!(open, json!({ "files": 2, "transferred": 1, "totalBytes": 1235000, "literalBytes": 4, "matchedBytes": 2000000, "sentBytes": 8, "receivedBytes": 9 }));
248 }
250 #[test]
251 fn the_summary_counts_what_will_be_transferred() {
252 let mut summary = Summary::default();
253 for line in [">f+++++++++\t10\ta", ">f.st......\t5\tb", ".f...p.....\t999\tc", "*deleting \t0\td", "cd+++++++++\t4096\te/"] {
254 summary.add(&item(line).unwrap());
255 }
256 assert_eq!(summary.to_json(), json!({ "new": 1, "changed": 1, "metadata": 1, "delete": 1, "directories": 1, "links": 0, "bytes": 15 }));
257 }
259 #[test]
260 fn exit_codes_are_explained() {
261 assert_eq!(exit_meaning(0).0, "ok");
262 assert_eq!(exit_meaning(24).0, "warning");
263 assert_eq!(exit_meaning(23).0, "partial");
264 assert_eq!(exit_meaning(25).0, "stopped");
265 assert_eq!(exit_meaning(255).0, "failed");
266 }
SHA-256989951c9aacee2b9324f952130b7f039338d0b04e427eb1c1e85b304a1ad0cb3

CH-J Proprietary Software License 1.14 · Die Quellen werden unter der CH-J Proprietary Software License 1.14 bereitgestellt. Ihre Verfügbarkeit ändert die Lizenzbedingungen nicht und gewährt keine zusätzlichen Rechte.

SHA-256 des Archivsac8fe790f4ba60fc098f8c985fc2656cfcc6cdc21fb34b30e308a2b54e62258f
Dateien