1use onestore::{
2 FileDataReference, RevisionIndex, Store,
3 document::{Document, Kind},
4};
5use std::{
6 borrow::Cow,
7 collections::BTreeSet,
8 env, fs,
9 io::{self, BufWriter, Write},
10 path::{Path, PathBuf},
11};
12
13fn write_json(
14 path: impl AsRef<Path>,
15 value: &impl serde::Serialize,
16) -> Result<(), Box<dyn std::error::Error>> {
17 let mut output = BufWriter::new(fs::File::create(path)?);
18 serde_json::to_writer(&mut output, value)?;
19 output.flush()?;
20 Ok(())
21}
22
23fn main() -> Result<(), Box<dyn std::error::Error>> {
24 let mut args = env::args_os().skip(1);
25 let path = args.next().ok_or("Provide a .one or .onetoc2 file.")?;
26 let next = args.next();
27 let (destination, option) = if next.as_deref() == Some(std::ffi::OsStr::new("--password-file"))
28 {
29 (None, next)
30 } else {
31 (next.map(PathBuf::from), args.next())
32 };
33 let password_file = match (option, args.next(), args.next()) {
34 (None, None, None) => None,
35 (Some(flag), Some(path), None) if flag == "--password-file" => Some(PathBuf::from(path)),
36 _ => return Err("Provide a source file, an optional new export directory, and optionally --password-file PATH.".into()),
37 };
38 let source_path = PathBuf::from(path);
39 let bytes = onestore::read_file(&source_path)?;
40 let store = Store::parse(&bytes)?;
41 let index = RevisionIndex::parse(&store)?;
42 let unlocked = if let Some(path) = &password_file {
43 use std::io::Read;
44 let mut password = zeroize::Zeroizing::new(String::new());
45 fs::File::open(path)?
46 .take(65537)
47 .read_to_string(&mut password)?;
48 if password.len() > 65536 {
49 return Err("The password file exceeds 64 KiB.".into());
50 }
51 Some(onestore::protected::UnlockedSection::open(
52 &index,
53 &password,
54 Default::default(),
55 )?)
56 } else {
57 None
58 };
59 let document = match &unlocked {
60 Some(section) => section.document()?,
61 None => Document::parse(&index)?,
62 };
63 if let Some(destination) = destination {
64 #[cfg(unix)]
65 {
66 use std::os::unix::fs::DirBuilderExt;
67 fs::DirBuilder::new()
68 .mode(if password_file.is_some() {
69 0o700
70 } else {
71 0o777
72 })
73 .create(&destination)?;
74 }
75 #[cfg(not(unix))]
76 fs::create_dir(&destination)?;
77 fs::create_dir(destination.join("assets"))?;
78 let parent = source_path
79 .parent()
80 .filter(|parent| !parent.as_os_str().is_empty())
81 .unwrap_or_else(|| Path::new("."));
82 let mut source = notebook::discover::Local::open(parent)?;
83 let section = source_path
84 .file_name()
85 .and_then(|name| name.to_str())
86 .ok_or("Use a UTF-8 section filename")?;
87 let mut assets = Vec::new();
88 let mut seen = BTreeSet::new();
89 for node in document
90 .spaces
91 .values()
92 .flat_map(|s| s.revisions.values())
93 .flat_map(|r| r.nodes.values())
94 {
95 if let Kind::File {
96 reference, payload, ..
97 } = &node.kind
98 && seen.insert(serde_json::to_string(reference)?)
99 {
100 let data = match reference {
101 FileDataReference::Internal(_) => {
102 Cow::Borrowed(payload.ok_or("Missing embedded file data")?)
103 }
104 FileDataReference::External(filename) => {
105 if password_file.is_some() {
106 assets.push(serde_json::json!({"reference":reference,"path":null,"error":{"kind":"Unsupported","message":"Protected external payload export is not supported"}}));
107 continue;
108 }
109 match notebook::discover::read_external_asset(
110 &mut source,
111 section,
112 filename,
113 256 * 1024 * 1024,
114 ) {
115 Ok(bytes) => Cow::Owned(bytes),
116 Err(error) => {
117 let kind = match &error {
118 notebook::discover::Error::Io { error, .. } => {
119 format!("{:?}", error.kind())
120 }
121 _ => "InvalidData".into(),
122 };
123 assets.push(serde_json::json!({"reference":reference,"path":null,"error":{"kind":kind,"message":error.to_string()}}));
124 continue;
125 }
126 }
127 }
128 FileDataReference::Invalid => {
129 assets.push(serde_json::json!({"reference":reference,"path":null,"error":{"kind":"InvalidData","message":"The document marks this payload as unavailable"}}));
130 continue;
131 }
132 };
133 let path = format!("assets/{}.bin", assets.len());
134 fs::write(destination.join(&path), data)?;
135 assets.push(serde_json::json!({"reference":reference,"path":path}));
136 }
137 }
138 write_json(destination.join("assets.json"), &assets)?;
139 let mut text = std::collections::BTreeMap::new();
140 for (sid, space) in &document.spaces {
141 let mut revisions = std::collections::BTreeMap::new();
142 for (rid, revision) in &space.revisions {
143 let mut objects = std::collections::BTreeMap::new();
144 for (oid, node) in &revision.nodes {
145 if matches!(node.kind, Kind::RichText { .. }) {
146 objects.insert(*oid, revision.text_runs(*oid)?);
147 }
148 }
149 revisions.insert(*rid, objects);
150 }
151 text.insert(*sid, revisions);
152 }
153 write_json(destination.join("text.json"), &text)?;
154 write_json(destination.join("document.json"), &document)?;
155 } else {
156 serde_json::to_writer(io::stdout().lock(), &document)?;
157 }
158 Ok(())
159}