libimaglinkcmd/
lib.rs

1//
2// imag - the personal information management suite for the commandline
3// Copyright (C) 2015-2020 Matthias Beyer <mail@beyermatthias.de> and contributors
4//
5// This library is free software; you can redistribute it and/or
6// modify it under the terms of the GNU Lesser General Public
7// License as published by the Free Software Foundation; version
8// 2.1 of the License.
9//
10// This library is distributed in the hope that it will be useful,
11// but WITHOUT ANY WARRANTY; without even the implied warranty of
12// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
13// Lesser General Public License for more details.
14//
15// You should have received a copy of the GNU Lesser General Public
16// License along with this library; if not, write to the Free Software
17// Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
18//
19
20#![forbid(unsafe_code)]
21
22#![deny(
23    non_camel_case_types,
24    non_snake_case,
25    path_statements,
26    trivial_numeric_casts,
27    unstable_features,
28    unused_allocation,
29    unused_import_braces,
30    unused_imports,
31    unused_must_use,
32    unused_mut,
33    unused_qualifications,
34    while_true,
35)]
36
37#[macro_use] extern crate log;
38extern crate clap;
39extern crate url;
40#[macro_use] extern crate failure;
41#[macro_use] extern crate prettytable;
42#[cfg(test)] extern crate toml;
43#[cfg(test)] extern crate toml_query;
44#[cfg(test)] extern crate env_logger;
45
46extern crate libimagentrylink;
47extern crate libimagentryurl;
48extern crate libimagrt;
49extern crate libimagstore;
50extern crate libimagerror;
51
52#[cfg(test)]
53#[macro_use]
54extern crate libimagutil;
55
56#[cfg(not(test))]
57extern crate libimagutil;
58
59use std::io::Write;
60use std::path::PathBuf;
61
62
63use failure::err_msg;
64
65use libimagentryurl::linker::UrlLinker;
66use libimagentrylink::linkable::Linkable;
67use libimagentrylink::storecheck::StoreLinkConsistentExt;
68use libimagrt::runtime::Runtime;
69use libimagrt::application::ImagApplication;
70use libimagstore::store::FileLockEntry;
71use libimagstore::storeid::StoreId;
72
73use url::Url;
74use failure::Fallible as Result;
75use clap::App;
76
77mod ui;
78
79/// Marker enum for implementing ImagApplication on
80///
81/// This is used by binaries crates to execute business logic
82/// or to build a CLI completion.
83pub enum ImagLink {}
84impl ImagApplication for ImagLink {
85    fn run(rt: Runtime) -> Result<()> {
86        if rt.cli().is_present("check-consistency") {
87            rt.store().check_link_consistency()?;
88            info!("Store is consistent");
89        }
90
91        if let Some(name) = rt.cli().subcommand_name() {
92            match name {
93                "remove" => remove_linking(&rt),
94                "unlink" => unlink(&rt),
95                "list"   => list_linkings(&rt),
96                other    => {
97                    debug!("Unknown command");
98                    if rt.handle_unknown_subcommand("imag-link", other, rt.cli())?.success() {
99                        Ok(())
100                    } else {
101                        Err(format_err!("Subcommand failed"))
102                    }
103                },
104            }
105        } else if let (Some(from), Some(to)) = (rt.cli().value_of("from"), rt.cli().values_of("to")) {
106            link_from_to(&rt, from, to)
107        } else {
108            Err(err_msg("No commandline call"))
109        }
110    }
111
112    fn build_cli<'a>(app: App<'a, 'a>) -> App<'a, 'a> {
113        ui::build_ui(app)
114    }
115
116    fn name() -> &'static str {
117        env!("CARGO_PKG_NAME")
118    }
119
120    fn description() -> &'static str {
121        "Link entries"
122    }
123
124    fn version() -> &'static str {
125        env!("CARGO_PKG_VERSION")
126    }
127}
128
129fn get_entry_by_name<'a>(rt: &'a Runtime, name: &str) -> Result<Option<FileLockEntry<'a>>> {
130    debug!("Getting: {:?}", name);
131    let result = StoreId::new(PathBuf::from(name)).and_then(|id| rt.store().get(id));
132
133    debug!(" => : {:?}", result);
134    result
135}
136
137fn link_from_to<'a, I>(rt: &'a Runtime, from: &'a str, to: I) -> Result<()>
138    where I: Iterator<Item = &'a str>
139{
140    let directional = rt.cli().is_present("directional");
141    let mut from_entry = get_entry_by_name(rt, from)?.ok_or_else(|| err_msg("No 'from' entry"))?;
142
143    for entry in to {
144        debug!("Handling 'to' entry: {:?}", entry);
145        if rt.store().get(PathBuf::from(entry))?.is_none() {
146            debug!("Linking externally: {:?} -> {:?}", from, entry);
147            let url = Url::parse(entry).map_err(|e| format_err!("Error parsing URL: {:?}", e))?;
148
149            let iter = from_entry
150                .add_url(rt.store(), url)?
151                .into_iter();
152
153            rt.report_all_touched(iter)?;
154        } else {
155            debug!("Linking internally: {:?} -> {:?}", from, entry);
156
157            let from_id = StoreId::new(PathBuf::from(from))?;
158            let entr_id = StoreId::new(PathBuf::from(entry))?;
159
160            if from_id == entr_id {
161                return Err(err_msg("Cannot link entry with itself. Exiting"))
162            }
163
164            let mut to_entry = rt
165                .store()
166                .get(entr_id)?
167                .ok_or_else(|| format_err!("No 'to' entry: {}", entry))?;
168
169            if directional {
170                from_entry.add_link_to(&mut to_entry)?;
171            } else {
172                from_entry.add_link(&mut to_entry)?;
173            }
174
175            rt.report_touched(to_entry.get_location())?;
176        }
177
178        info!("Ok: {} -> {}", from, entry);
179    }
180
181    rt.report_touched(from_entry.get_location())
182}
183
184fn remove_linking(rt: &Runtime) -> Result<()> {
185    let mut from : FileLockEntry = rt.cli()
186        .subcommand_matches("remove")
187        .unwrap() // safe, we know there is an "remove" subcommand
188        .value_of("from")
189        .map(PathBuf::from)
190        .and_then(|id| rt.store().get(id).transpose())
191        .ok_or_else(|| err_msg("No 'from' entry"))??;
192
193    rt
194        .ids::<crate::ui::PathProvider>()?
195        .ok_or_else(|| err_msg("No ids supplied"))?
196        .into_iter()
197        .map(|id| match rt.store().get(id.clone())? {
198            Some(mut to_entry) => {
199                to_entry.remove_link(&mut from)?;
200                rt.report_touched(to_entry.get_location())
201            },
202
203            None => {
204                // looks like this is not an entry, but a filesystem URI and therefor an
205                // external link...?
206                if id.local().is_file() {
207                    let pb = id.local().to_str().ok_or_else(|| format_err!("Not StoreId and not a Path: {}", id))?;
208                    let url = Url::parse(pb).map_err(|e| format_err!("Error parsing URL: {:?}", e))?;
209                    from.remove_url(rt.store(), url)?;
210                    info!("Ok: {}", id);
211                    Ok(())
212                } else {
213                    Err(format_err!("Entry not found: {:?}", id))
214                }
215            }
216        })
217        .collect::<Result<Vec<_>>>()?;
218
219    rt.report_touched(from.get_location())
220}
221
222fn unlink(rt: &Runtime) -> Result<()> {
223    rt
224        .ids::<crate::ui::PathProvider>()?
225        .ok_or_else(|| err_msg("No ids supplied"))?
226        .into_iter()
227        .map(|id| {
228            rt.store()
229                .get(id.clone())?
230                .ok_or_else(|| format_err!("No entry for {}", id))?
231                .unlink(rt.store())?;
232
233            rt.report_touched(&id)
234        })
235        .collect()
236}
237
238fn list_linkings(rt: &Runtime) -> Result<()> {
239    let cmd = rt.cli()
240        .subcommand_matches("list")
241        .unwrap(); // safed by clap
242
243    let list_externals  = cmd.is_present("list-externals-too");
244    let list_plain      = cmd.is_present("list-plain");
245
246    let mut tab = ::prettytable::Table::new();
247    tab.set_titles(row!["#", "Link"]);
248
249    rt.ids::<crate::ui::PathProvider>()?
250        .ok_or_else(|| err_msg("No ids supplied"))?
251        .into_iter()
252        .map(|id| {
253            let entry = rt.store().get(id.clone())?.ok_or_else(|| format_err!("Not found: {}", id))?;
254
255            for (i, link) in entry.links()?.enumerate() {
256                let link = link.to_str()?;
257
258                if list_plain {
259                    writeln!(rt.stdout(), "{: <3}: {}", i, link)?;
260                } else {
261                    tab.add_row(row![i, link]);
262                }
263            }
264
265            if list_externals {
266                entry.get_urls(rt.store())?
267                    .enumerate()
268                    .map(|(i, link)| {
269                        let link = link?.into_string();
270
271                        if list_plain {
272                            writeln!(rt.stdout(), "{: <3}: {}", i, link)?;
273                        } else {
274                            tab.add_row(row![i, link]);
275                        }
276
277                        Ok(())
278                    })
279                    .collect::<Result<Vec<_>>>()?;
280            }
281
282            rt.report_touched(entry.get_location())
283        })
284        .collect::<Result<Vec<_>>>()?;
285
286    if !list_plain {
287        let out      = rt.stdout();
288        let mut lock = out.lock();
289        tab.print(&mut lock)?;
290    }
291
292    Ok(())
293}
294
295#[cfg(test)]
296mod tests {
297    use super::link_from_to;
298    use super::remove_linking;
299
300    use std::path::PathBuf;
301    use std::ffi::OsStr;
302
303    use toml::value::Value;
304    use toml_query::read::TomlValueReadExt;
305    use failure::Fallible as Result;
306    use failure::Error;
307
308    use libimagrt::runtime::Runtime;
309    use libimagstore::storeid::StoreId;
310    use libimagstore::store::{FileLockEntry, Entry};
311
312    fn setup_logging() {
313        let _ = ::env_logger::try_init();
314    }
315
316    make_mock_app! {
317        app "imag-link";
318        modulename mock;
319        version env!("CARGO_PKG_VERSION");
320        with help "imag-link mocking app";
321        with ui builder function crate::ui::build_ui;
322    }
323    use self::mock::generate_test_runtime;
324    use self::mock::reset_test_runtime;
325
326    fn create_test_default_entry<'a, S: AsRef<OsStr>>(rt: &'a Runtime, name: S) -> Result<StoreId> {
327        let mut path = PathBuf::new();
328        path.set_file_name(name);
329
330        let default_entry = Entry::new(StoreId::new(PathBuf::from("")).unwrap())
331            .to_str()
332            .unwrap();
333
334        debug!("Default entry constructed");
335
336        let id = StoreId::new(path)?;
337        debug!("StoreId constructed: {:?}", id);
338
339        let mut entry = rt.store().create(id.clone())?;
340
341        debug!("Entry constructed: {:?}", id);
342        entry.get_content_mut().push_str(&default_entry);
343
344        Ok(id)
345    }
346
347    fn get_entry_links<'a>(entry: &'a FileLockEntry<'a>) -> Result<&'a Value> {
348        match entry.get_header().read(&"links.internal".to_owned()).map_err(Error::from)? {
349            Some(v) => Ok(v),
350            None    => panic!("Didn't find 'links' in {:?}", entry),
351        }
352    }
353
354    fn links_toml_value<I: IntoIterator<Item = &'static str>>(links: I) -> Value {
355        Value::Array(links
356                         .into_iter()
357                         .map(|s| Value::String(s.to_owned()))
358                         .collect())
359    }
360
361    #[test]
362    fn test_link_modificates() {
363        setup_logging();
364        let rt = generate_test_runtime(vec!["test1", "test2"])
365            .unwrap();
366
367        debug!("Runtime created");
368
369        let test_id1 = create_test_default_entry(&rt, "test1").unwrap();
370        let test_id2 = create_test_default_entry(&rt, "test2").unwrap();
371
372        debug!("Entries created");
373
374        link_from_to(&rt, "test1", vec!["test2"].into_iter()).unwrap();
375
376        debug!("Linking done");
377
378        let test_entry1 = rt.store().get(test_id1).unwrap().unwrap();
379        let test_links1 = get_entry_links(&test_entry1).unwrap();
380
381        let test_entry2 = rt.store().get(test_id2).unwrap().unwrap();
382        let test_links2 = get_entry_links(&test_entry2).unwrap();
383
384        debug!("Asserting");
385
386        assert_ne!(*test_links1, links_toml_value(vec![]));
387        assert_ne!(*test_links2, links_toml_value(vec![]));
388
389        debug!("Test finished")
390    }
391
392    #[test]
393    fn test_linking_links() {
394        setup_logging();
395        let rt = generate_test_runtime(vec!["test1", "test2"])
396            .unwrap();
397
398        debug!("Runtime created");
399
400        let test_id1 = create_test_default_entry(&rt, "test1").unwrap();
401        let test_id2 = create_test_default_entry(&rt, "test2").unwrap();
402
403        debug!("Test entries created");
404
405        link_from_to(&rt, "test1", vec!["test2"].into_iter()).unwrap();
406
407        debug!("Linking done");
408
409        let test_entry1 = rt.store().get(test_id1).unwrap().unwrap();
410        let test_links1 = get_entry_links(&test_entry1).unwrap();
411
412        let test_entry2 = rt.store().get(test_id2).unwrap().unwrap();
413        let test_links2 = get_entry_links(&test_entry2).unwrap();
414
415        debug!("Asserting");
416
417        assert_eq!(*test_links1, links_toml_value(vec!["test2"]));
418        assert_eq!(*test_links2, links_toml_value(vec!["test1"]));
419    }
420
421    #[test]
422    fn test_multilinking() {
423        setup_logging();
424        let rt = generate_test_runtime(vec!["test1", "test2"])
425            .unwrap();
426
427        debug!("Runtime created");
428
429        let test_id1 = create_test_default_entry(&rt, "test1").unwrap();
430        let test_id2 = create_test_default_entry(&rt, "test2").unwrap();
431
432        debug!("Test entries created");
433
434        link_from_to(&rt, "test1", vec!["test2"].into_iter()).unwrap();
435        link_from_to(&rt, "test1", vec!["test2"].into_iter()).unwrap();
436
437        debug!("Linking done");
438
439        let test_entry1 = rt.store().get(test_id1).unwrap().unwrap();
440        let test_links1 = get_entry_links(&test_entry1).unwrap();
441
442        let test_entry2 = rt.store().get(test_id2).unwrap().unwrap();
443        let test_links2 = get_entry_links(&test_entry2).unwrap();
444
445        debug!("Asserting");
446
447        assert_eq!(*test_links1, links_toml_value(vec!["test2"]));
448        assert_eq!(*test_links2, links_toml_value(vec!["test1"]));
449    }
450
451    #[test]
452    fn test_linking_more_than_two() {
453        setup_logging();
454        let rt = generate_test_runtime(vec!["test1", "test2", "test3"])
455            .unwrap();
456
457        debug!("Runtime created");
458
459        let test_id1 = create_test_default_entry(&rt, "test1").unwrap();
460        let test_id2 = create_test_default_entry(&rt, "test2").unwrap();
461        let test_id3 = create_test_default_entry(&rt, "test3").unwrap();
462
463        debug!("Test entries created");
464
465        link_from_to(&rt, "test1", vec!["test2", "test3"].into_iter()).unwrap();
466        link_from_to(&rt, "test1", vec!["test2", "test3"].into_iter()).unwrap();
467
468        debug!("Linking done");
469
470        let test_entry1 = rt.store().get(test_id1).unwrap().unwrap();
471        let test_links1 = get_entry_links(&test_entry1).unwrap();
472
473        let test_entry2 = rt.store().get(test_id2).unwrap().unwrap();
474        let test_links2 = get_entry_links(&test_entry2).unwrap();
475
476        let test_entry3 = rt.store().get(test_id3).unwrap().unwrap();
477        let test_links3 = get_entry_links(&test_entry3).unwrap();
478
479        debug!("Asserting");
480
481        assert_eq!(*test_links1, links_toml_value(vec!["test2", "test3"]));
482        assert_eq!(*test_links2, links_toml_value(vec!["test1"]));
483        assert_eq!(*test_links3, links_toml_value(vec!["test1"]));
484    }
485
486    // Remove tests
487
488    #[test]
489    fn test_linking_links_unlinking_removes_links() {
490        setup_logging();
491        let rt = generate_test_runtime(vec!["test1", "test2"])
492            .unwrap();
493
494        debug!("Runtime created");
495
496        let test_id1 = create_test_default_entry(&rt, "test1").unwrap();
497        let test_id2 = create_test_default_entry(&rt, "test2").unwrap();
498
499        debug!("Test entries created");
500
501        link_from_to(&rt, "test1", vec!["test2"].into_iter()).unwrap();
502
503        debug!("Linking done");
504
505        let rt = reset_test_runtime(vec!["remove", "test1", "test2"], rt)
506            .unwrap();
507
508        remove_linking(&rt).unwrap();
509
510        debug!("Linking removed");
511
512        let test_entry1 = rt.store().get(test_id1).unwrap().unwrap();
513        let test_links1 = get_entry_links(&test_entry1).unwrap();
514
515        let test_entry2 = rt.store().get(test_id2).unwrap().unwrap();
516        let test_links2 = get_entry_links(&test_entry2).unwrap();
517
518        debug!("Asserting");
519
520        assert_eq!(*test_links1, links_toml_value(vec![]));
521        assert_eq!(*test_links2, links_toml_value(vec![]));
522    }
523
524    #[test]
525    fn test_linking_and_unlinking_more_than_two() {
526        setup_logging();
527        let rt = generate_test_runtime(vec!["test1", "test2", "test3"])
528            .unwrap();
529
530        debug!("Runtime created");
531
532        let test_id1 = create_test_default_entry(&rt, "test1").unwrap();
533        let test_id2 = create_test_default_entry(&rt, "test2").unwrap();
534        let test_id3 = create_test_default_entry(&rt, "test3").unwrap();
535
536        debug!("Test entries created");
537
538        link_from_to(&rt, "test1", vec!["test2", "test3"].into_iter()).unwrap();
539
540        debug!("linking done");
541
542        let rt = reset_test_runtime(vec!["remove", "test1", "test2", "test3"], rt)
543            .unwrap();
544
545        remove_linking(&rt).unwrap();
546
547        debug!("linking removed");
548
549        let test_entry1 = rt.store().get(test_id1).unwrap().unwrap();
550        let test_links1 = get_entry_links(&test_entry1).unwrap();
551
552        let test_entry2 = rt.store().get(test_id2).unwrap().unwrap();
553        let test_links2 = get_entry_links(&test_entry2).unwrap();
554
555        let test_entry3 = rt.store().get(test_id3).unwrap().unwrap();
556        let test_links3 = get_entry_links(&test_entry3).unwrap();
557
558        debug!("Asserting");
559
560        assert_eq!(*test_links1, links_toml_value(vec![]));
561        assert_eq!(*test_links2, links_toml_value(vec![]));
562        assert_eq!(*test_links3, links_toml_value(vec![]));
563    }
564}