Nailathalang source
I-download ang source ZIP
CH-J Tools
I-browse ang mga direktoryo at file ng isang partikular na release ng application.
CH-J / Server Manager
1704 file
0.1.2-beta
Idinagdag: · Binuo:
1
//! Reads what rsync prints: one line per change, the overall progress and the closing statistics.3
use once_cell::sync::Lazy;4
use regex::Regex;5
use serde_json::{json, Value};7
#[derive(Debug, PartialEq, Clone)]8
pub enum Change {9
New,10
Changed,11
MetadataOnly,12
Delete,13
Directory,14
Link,15
}17
impl 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
}28
}30
#[derive(Debug, PartialEq, Clone)]31
pub struct Item {32
pub change: Change,33
pub size: u64,34
pub path: String,35
pub flags: String,36
}38
/// rsync writes unusual bytes of a name as `\#ooo`.39
pub 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()54
}56
fn 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
})77
}79
/// One line of `--out-format=%i<TAB>%l<TAB>%n`, or a plain `*deleting name` line.80
pub 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() })90
}92
/// ` 1,234,567 45% 1.20MB/s 0:00:12 (xfr#3, to-chk=1/4)`.93
pub 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] }))98
}100
/// `1,234 bytes`, or the rounded `1235 kB` of openrsync.101
fn 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
})111
}113
/// The closing block of `--stats`. The wording differs a little between rsync and openrsync.114
pub 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)140
}142
#[derive(Default)]143
pub 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,151
}153
impl 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
}174
}176
/// What an exit code of rsync means for the user. Code 24 means files vanished while reading, normal on a live server.177
pub 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
}191
}193
#[cfg(test)]194
mod 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
}267
}SHA-256
989951c9aacee2b9324f952130b7f039338d0b04e427eb1c1e85b304a1ad0cb3CH-J Proprietary Software License 1.14 · Ibinibigay ang source sa ilalim ng CH-J Proprietary Software License 1.14. Hindi binabago ng pagiging available nito ang mga tuntunin ng lisensya at hindi ito nagbibigay ng karagdagang karapatan.
SHA-256 ng archive
ac8fe790f4ba60fc098f8c985fc2656cfcc6cdc21fb34b30e308a2b54e62258f