Skip to content

Commit

Permalink
Merge pull request #2 from pkgw/work2
Browse files Browse the repository at this point in the history
More work
  • Loading branch information
pkgw authored Dec 4, 2018
2 parents cf8bcf4 + e9483fb commit fd5c1e3
Show file tree
Hide file tree
Showing 4 changed files with 249 additions and 45 deletions.
23 changes: 23 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ google-drive3 = "1.0"
google-people1 = "1.0"
hyper = "^0.10" # intentionally old version of Hyper
hyper-native-tls = "^0.3"
petgraph = "^0.4"
serde = "^1.0"
serde_derive = "^1.0"
serde_json = "^1.0"
Expand Down
151 changes: 151 additions & 0 deletions src/app.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@
use diesel::prelude::*;
use diesel::sqlite::SqliteConnection;
use petgraph::prelude::*;
use std::collections::HashMap;
use yup_oauth2::ApplicationSecret;

use accounts::{self, Account};
Expand Down Expand Up @@ -178,3 +180,152 @@ impl Application {
Ok(())
}
}


/// Data about inter-document linkages.
///
/// We have a database table that can store the inter-document linkage
/// information, but I'm pretty sure that for Real Computations it quickly
/// becomes very unhealthy to do them using the database. So instead, when
/// asked we construct an in-memory `petgraph` graph from the database
/// contents.
pub struct LinkageTable {
graph: petgraph::Graph<String, (), Directed, u32>,
nodes: HashMap<String, NodeIndex<u32>>
}

impl Application {
/// Load the table of inter-document linkages.
///
/// The underlying graph is directed. If `transpose` is false, links will
/// point from parents to children. If true, links will point from
/// children to parents.
pub fn load_linkage_table(&self, transpose: bool) -> Result<LinkageTable> {
use schema::links::dsl::*;

let mut graph = petgraph::Graph::new();
let mut nodes = HashMap::new();

for link in links.load::<database::Link>(&self.conn)? {
let pix = *nodes.entry(link.parent_id.clone()).or_insert_with(
|| graph.add_node(link.parent_id.clone())
);
let cix = *nodes.entry(link.child_id.clone()).or_insert_with(
|| graph.add_node(link.child_id.clone())
);

// The `update_edge()` function prevents duplicate edges from
// being formed, but because the database has a primary key
// constraint on the pair (parent_id, child_id), it should be
// impossible for this function to attempt to create such
// duplications.

if transpose {
graph.add_edge(cix, pix, ());
} else {
graph.add_edge(pix, cix, ());
}
}

Ok(LinkageTable { graph, nodes })
}
}


impl LinkageTable {
/// Given a document ID, find the set of folders that contain it.
///
/// This is nontrivial because in Google Drive, the folder structure can
/// basically be an arbitrary graph -- including cycles.
///
/// The *self* linkage table must have been loaded with *transpose* set to
/// true, so that the graph edges point from children to parents.
///
/// The return value is a vector of paths, because the document can have
/// multiple parents, or one of its parents might have multiple parents.
/// Each path is itself a vector of document IDs. The first item in the
/// vector is the outermost folder, while the last item is the folder
/// containing the target document. The ID of the target document is not
/// included in the return values. The path may be an empty vector, if the
/// document has been shared with the user's account but not "added to My
/// Drive". (And perhaps other reasons?)
///
/// The algorithm here is homebrewed because I couldn't find any serious
/// discussion of the relevant graph-thory problem. It's basically a
/// breadth-first iteration, but it is willing to revisit nodes so long as
/// they do not create a cycle within the path being considered.
pub fn find_parent_paths(&self, start_id: &str) -> Vec<Vec<String>> {
use std::collections::HashSet;

let roots: HashSet<NodeIndex> = self.graph.externals(Direction::Outgoing).collect();

let start_ix = match self.nodes.get(start_id) {
Some(ix) => *ix,
None => return Vec::new()
};

let mut queue = Vec::new();
queue.push(start_ix);

let mut path_data = HashMap::new();
path_data.insert(start_ix, None);

let mut results = Vec::new();

while queue.len() > 0 {
let cur_ix = queue.pop().unwrap();

if roots.contains(&cur_ix) {
// We finished a path!
let mut path = Vec::new();
let mut ix = cur_ix;

// Can't do this as a `while let` loop since the bindings shadow
loop {
if let Some(new_ix) = path_data.get(&ix).unwrap() {
path.push(self.graph.node_weight(ix).unwrap().clone());
ix = *new_ix;
} else {
break;
}
}

//path.reverse();
results.push(path);
}

for next_ix in self.graph.neighbors(cur_ix) {
// Already enqueued?
if queue.contains(&next_ix) {
continue;
}

// Check for loops.
let mut ix = cur_ix;

let found_loop = loop {
if ix == next_ix {
break true;
}

if let Some(new_ix) = path_data.get(&ix).unwrap() {
ix = *new_ix;
} else {
break false;
}
};

if found_loop {
continue;
}

// Looks like we should consider this node.

path_data.insert(next_ix, Some(cur_ix));
queue.push(next_ix);
}
}

results
}
}
119 changes: 74 additions & 45 deletions src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ extern crate google_drive3;
extern crate google_people1;
extern crate hyper;
extern crate hyper_native_tls;
extern crate petgraph;
extern crate serde;
#[macro_use] extern crate serde_derive;
extern crate serde_json;
Expand Down Expand Up @@ -64,6 +65,74 @@ fn open_url<S: AsRef<OsStr>>(url: S) -> Result<()> {
}


/// Show detailed information about one or more documents.
#[derive(Debug, StructOpt)]
pub struct DrorgInfoOptions {
#[structopt(long = "no-sync", help = "Do not attempt to synchronize with the Google servers")]
no_sync: bool,

#[structopt(help = "A document name, or fragment thereof")]
stem: String,
}

impl DrorgInfoOptions {
fn cli(self, mut app: Application) -> Result<i32> {
use schema::docs::dsl::*;

if !self.no_sync {
app.sync_all_accounts()?;
}

let linkages = app.load_linkage_table(true)?;

let pattern = format!("%{}%", self.stem);
let results = docs.filter(name.like(&pattern))
.load::<database::Doc>(&app.conn)?;
let mut first = true;

for doc in results {
if first {
first = false;
} else {
println!("");
}

println!("Name: {}", doc.name);
println!("MIME-type: {}", doc.mime_type);
println!("Modified: {}", doc.utc_mod_time().to_rfc3339());
println!("ID: {}", doc.id);
println!("Starred?: {}", if doc.starred { "yes" } else { "no" });
println!("Trashed?: {}", if doc.trashed { "yes" } else { "no" });

let paths: Vec<_> = linkages.find_parent_paths(&doc.id).iter().map(|id_path| {
// This is not efficient, and it's panicky, but meh.
let names: Vec<_> = id_path.iter().map(|docid| {
let elem = docs.filter(id.eq(&docid))
.load::<database::Doc>(&app.conn).unwrap();
assert_eq!(elem.len(), 1);
elem[0].name.clone()
}).collect();

names.join(" > ")
}).collect();

match paths.len() {
0 => println!("Path: [none -- root folder?]"),
1 => println!("Path: {}", paths[0]),
_n => {
println!("Paths::");
for path in paths {
println!(" {}", path);
}
}
}
}

Ok(0)
}
}


/// Temp? List documents.
#[derive(Debug, StructOpt)]
pub struct DrorgListOptions {
Expand Down Expand Up @@ -216,50 +285,14 @@ impl DrorgResyncOptions {
}


/// Temp debugging
#[derive(Debug, StructOpt)]
pub struct DrorgTempOptions {}

impl DrorgTempOptions {
fn cli(self, app: Application) -> Result<i32> {
for maybe_info in accounts::get_accounts()? {
let (email, mut account) = maybe_info?;

let token = account.data.change_page_token.take().ok_or(
format_err!("no paging token for {}", email)
)?;

let token = account.with_drive_hub(&app.secret, |hub| {
let mut lister = google_apis::list_changes(
&hub, &token,
|call| call.spaces("drive")
.supports_team_drives(true)
.include_team_drive_items(true)
.include_removed(true)
.include_corpus_removals(true)
);

for maybe_change in lister.iter() {
let change = maybe_change?;
println!("{:?}", change);
}

Ok(lister.into_change_page_token())
})?;

account.data.change_page_token = Some(token);
account.save_to_json()?;
}

Ok(0)
}
}


/// The main StructOpt type for dispatching subcommands.
#[derive(Debug, StructOpt)]
#[structopt(name = "drorg", about = "Organize documents on Google Drive.")]
pub enum DrorgCli {
#[structopt(name = "info")]
/// Show detailed information about one or more documents
Info(DrorgInfoOptions),

#[structopt(name = "list")]
/// List documents
List(DrorgListOptions),
Expand All @@ -275,22 +308,18 @@ pub enum DrorgCli {
#[structopt(name = "resync")]
/// Re-synchronize with an account
Resync(DrorgResyncOptions),

#[structopt(name = "temp")]
/// Temporary dev work
Temp(DrorgTempOptions),
}

impl DrorgCli {
fn cli(self) -> Result<i32> {
let app = Application::initialize()?;

match self {
DrorgCli::Info(opts) => opts.cli(app),
DrorgCli::List(opts) => opts.cli(app),
DrorgCli::Login(opts) => opts.cli(app),
DrorgCli::Open(opts) => opts.cli(app),
DrorgCli::Resync(opts) => opts.cli(app),
DrorgCli::Temp(opts) => opts.cli(app),
}
}
}
Expand Down

0 comments on commit fd5c1e3

Please sign in to comment.