import com.dropbox.client2.exception.DropboxException;
import com.dropbox.client2.session.Session;
import com.dropbox.client2.session.WebAuthSession;
import com.dropbox.client2.session.AppKeyPair;
import com.dropbox.client2.session.AccessTokenPair;
import com.dropbox.client2.DropboxAPI;
import com.dropbox.client2.DropboxAPI.DeltaEntry;
import com.dropbox.client2.jsonextract.*;
import org.json.simple.JSONArray;
import org.json.simple.JSONObject;
import org.json.simple.parser.JSONParser;
import org.json.simple.parser.ParseException;
import java.io.*;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.Map;
public class SearchCache
{
public static final String STATE_FILE = "SearchCache.json";
public static void main(String[] args)
throws DropboxException
{
if (args.length == 0) {
printUsage(System.out);
throw die();
}
String command = args[0];
if (command.equals("link")) {
doLink(args);
}
else if (command.equals("update")) {
doUpdate(args);
}
else if (command.equals("find")) {
doFind(args);
}
else if (command.equals("reset")) {
doReset(args);
}
else {
System.err.println("ERROR: Unknown command: \"" + command + "\"");
System.err.println("Run with no arguments for help.");
throw die();
}
}
private static void doLink(String[] args)
throws DropboxException
{
if (args.length != 3) {
throw die("ERROR: \"link\" takes exactly two arguments.");
}
AppKeyPair appKeyPair = new AppKeyPair(args[1], args[2]);
WebAuthSession was = new WebAuthSession(appKeyPair, Session.AccessType.APP_FOLDER);
// Make the user log in and authorize us.
WebAuthSession.WebAuthInfo info = was.getAuthInfo();
System.out.println("1. Go to: " + info.url);
System.out.println("2. Allow access to this app.");
System.out.println("3. Press ENTER.");
try {
while (System.in.read() != '\n') {}
}
catch (IOException ex) {
throw die("I/O error: " + ex.getMessage());
}
// This will fail if the user didn't visit the above URL and hit 'Allow'.
was.retrieveWebAccessToken(info.requestTokenPair);
AccessTokenPair accessToken = was.getAccessTokenPair();
System.out.println("Link successful.");
// Save state
State state = new State(appKeyPair, accessToken, new Content.Folder());
state.save(STATE_FILE);
}
private static void doUpdate(String[] args)
throws DropboxException
{
int pageLimit;
if (args.length == 2) {
pageLimit = Integer.parseInt(args[1]);
}
else if (args.length == 1) {
pageLimit = -1;
}
else {
throw die("ERROR: \"update\" takes either zero or one arguments.");
}
// Load state.
State state = State.load(STATE_FILE);
// Connect to Dropbox.
WebAuthSession session = new WebAuthSession(state.appKey, WebAuthSession.AccessType.APP_FOLDER);
session.setAccessTokenPair(state.accessToken);
DropboxAPI<?> client = new DropboxAPI<WebAuthSession>(session);
int pageNum = 0;
boolean changed = false;
String cursor = state.cursor;
while (pageLimit < 0 || (pageNum < pageLimit)) {
// Get /delta results from Dropbox
DropboxAPI.DeltaPage<DropboxAPI.Entry> page = client.delta(cursor);
pageNum++;
if (page.reset) {
state.tree.children.clear();
changed = true;
}
// Apply the entries one by one.
for (DeltaEntry<DropboxAPI.Entry> e : page.entries) {
applyDelta(state.tree, e);
changed = true;
}
cursor = page.cursor;
if (!page.hasMore) break;
}
// Save state.
if (changed) {
state.cursor = cursor;
state.save(STATE_FILE);
}
else {
System.out.println("No updates.");
}
}
private static void printUsage(PrintStream out)
{
out.println("Usage:");
out.println(" ./run link <app-key> <secret> Link a user's account to the given app.");
out.println(" ./run update Update cache to the latest on Dropbox.");
out.println(" ./run update <num> Update cache, limit to <num> pages of updates.");
out.println(" ./run find <term> Search cache for <term> (case-sensitive).");
out.println(" ./run find Display entire cache.");
out.println(" ./run reset Delete the cache.");
}
private static RuntimeException die(String message)
{
System.err.println(message);
return die();
}
private static RuntimeException die()
{
System.exit(1);
return new RuntimeException();
}
// ------------------------------------------------------------------------
// Apply delta entries to the tree.
private static void applyDelta(Content.Folder parent, DeltaEntry<DropboxAPI.Entry> e)
{
Path path = Path.parse(e.lcPath);
DropboxAPI.Entry md = e.metadata;
if (md != null) {
System.out.println("+ " + e.lcPath);
// Traverse down the tree until we find the parent of the entry we
// want to add. Create any missing folders along the way.
for (String b : path.branch) {
Node n = getOrCreateChild(parent, b);
if (n.content instanceof Content.Folder) {
parent = (Content.Folder) n.content;
} else {
// No folder here, automatically create an empty one.
n.content = parent = new Content.Folder();
}
}
// Create the file/folder here.
Node n = getOrCreateChild(parent, path.leaf);
n.path = md.path; // Save the un-lower-cased path.
if (md.isDir) {
// Only create an empty folder if there isn't one there already.
if (!(n.content instanceof Content.Folder)) {
n.content = new Content.Folder();
}
}
else {
n.content = new Content.File(md.size, md.modified, md.clientMtime);
}
}
else {
System.out.println("- " + e.lcPath);
// Traverse down the tree until we find the parent of the entry we
// want to delete.
boolean missingParent = false;
for (String b : path.branch) {
Node n = parent.children.get(b);
if (n != null && n.content instanceof Content.Folder) {
parent = (Content.Folder) n.content;
} else {
// If one of the parent folders is missing, then we're done.
missingParent = true;
break;
}
}
if (!missingParent) {
parent.children.remove(path.leaf);
}
}
}
private static Node getOrCreateChild(Content.Folder folder, String lowercaseName)
{
Node n = folder.children.get(lowercaseName);
if (n == null) {
folder.children.put(lowercaseName, n = new Node(null, null));
}
return n;
}
/**
* Represent a path as a list of ancestors and a leaf name.
*
* For example, "/a/b/c" -> Path(["a", "b"], "c")
*/
public static final class Path
{
public final String[] branch;
public final String leaf;
public Path(String[] branch, String leaf)
{
assert branch != null;
assert leaf != null;
this.branch = branch;
this.leaf = leaf;
}
public static Path parse(String s)
{
assert s.startsWith("/");
String[] parts = s.split("/");
assert parts.length > 0;
String[] branch = new String[parts.length-2];
System.arraycopy(parts, 1, branch, 0, branch.length);
String leaf = parts[parts.length-1];
return new Path(branch, leaf);
}
}
// ------------------------------------------------------------------------
// Search through the tree.
private static void doFind(String[] args)
throws DropboxException
{
String term;
if (args.length == 1) {
term = "";
}
else if (args.length == 2) {
term = args[1];
}
else {
throw die("ERROR: \"find\" takes either zero or one arguments");
}
// Load cached state.
State state = State.load(STATE_FILE);
ArrayList<String> results = new ArrayList<String>();
searchTree(results, state.tree, term);
for (String r : results) {
System.out.println(r);
}
if (results.isEmpty()) {
System.out.println("[No matches.]");
}
}
private static void searchTree(ArrayList<String> results, Content.Folder tree, String term)
{
for (Map.Entry<String,Node> child : tree.children.entrySet()) {
Node n = child.getValue();
String path = n.path;
if (path != null && path.contains(term)) {
if (n.content instanceof Content.Folder) {
results.add(path);
}
else if (n.content instanceof Content.File) {
Content.File f = (Content.File) n.content;
results.add(path + " (" + f.size + ", " + f.lastModified + ", " + f.clientMtime + ")");
}
else {
throw new AssertionError("bad type: " + n.content);
}
}
// Recurse on children.
if (n.content instanceof Content.Folder) {
Content.Folder f = (Content.Folder) n.content;
searchTree(results, f, term);
}
}
}
// ------------------------------------------------------------------------
// Reset state
private static void doReset(String[] args)
throws DropboxException
{
if (args.length != 1) {
throw die("ERROR: \"reset\" takes no arguments");
}
// Load state.
State state = State.load(STATE_FILE);
// Clear state.
state.tree.children.clear();
state.cursor = null;
// Save state back.
state.save(STATE_FILE);
}
// ------------------------------------------------------------------------
// State model (load+save to JSON)
public static final class State
{
public final AppKeyPair appKey;
public final AccessTokenPair accessToken;
public final Content.Folder tree;
public State(AppKeyPair appKey, AccessTokenPair accessToken, Content.Folder tree)
{
this.appKey = appKey;
this.accessToken = accessToken;
this.tree = tree;
}
public String cursor;
public void save(String fileName)
{
JSONObject jstate = new JSONObject();
// Convert app key
JSONArray japp = new JSONArray();
japp.add(appKey.key);
japp.add(appKey.secret);
jstate.put("app_key", japp);
// Convert access token
JSONArray jaccess = new JSONArray();
jaccess.add(accessToken.key);
jaccess.add(accessToken.secret);
jstate.put("access_token", jaccess);
// Convert tree
JSONObject jtree = tree.toJson();
jstate.put("tree", jtree);
// Convert cursor, if present.
if (cursor != null) {
jstate.put("cursor", cursor);
}
try {
FileWriter fout = new FileWriter(fileName);
try {
jstate.writeJSONString(fout);
}
finally {
fout.close();
}
}
catch (IOException ex) {
throw die("ERROR: unable to save to state file \"" + fileName + "\": " + ex.getMessage());
}
}
public static State load(String fileName)
{
JsonThing j;
try {
FileReader fin = new FileReader(fileName);
try {
j = new JsonThing(new JSONParser().parse(fin));
} catch (ParseException ex) {
throw die("ERROR: State file \"" + fileName + "\" isn't valid JSON: " + ex.getMessage());
} finally {
fin.close();
}
}
catch (IOException ex) {
throw die("ERROR: unable to load state file \"" + fileName + "\": " + ex.getMessage());
}
try {
JsonMap jm = j.expectMap();
JsonList japp = jm.get("app_key").expectList();
AppKeyPair appKey = new AppKeyPair(japp.get(0).expectString(), japp.get(1).expectString());
JsonList jaccess = jm.get("access_token").expectList();
AccessTokenPair accessToken = new AccessTokenPair(jaccess.get(0).expectString(), jaccess.get(1).expectString());
JsonMap jtree = jm.get("tree").expectMap();
Content.Folder tree = Content.Folder.fromJson(jtree);
State state = new State(appKey, accessToken, tree);
JsonThing jcursor = jm.getMaybe("cursor");
if (jcursor != null) {
state.cursor = jcursor.expectString();
}
return state;
}
catch (JsonExtractionException ex) {
throw die ("ERROR: State file has incorrect structure: " + ex.getMessage());
}
}
}
// ------------------------------------------------------------------------
// We represent our local cache as a tree of 'Node' objects.
public static final class Node
{
/**
* The original path of the file. We track this separately because
* Folder.children only contains lower-cased names.
*/
public String path;
/**
* The node content (either Content.File or Content.Folder)
*/
public Content content;
public Node(String path, Content content)
{
this.path = path;
this.content = content;
}
public final JSONArray toJson()
{
JSONArray array = new JSONArray();
array.add(path);
array.add(content.toJson());
return array;
}
public static Node fromJson(JsonThing t)
throws JsonExtractionException
{
JsonList l = t.expectList();
String path = l.get(0).expectStringOrNull();
JsonThing jcontent = l.get(1);
Content content;
if (jcontent.isList()) {
content = Content.File.fromJson(jcontent.expectList());
} else if (jcontent.isMap()) {
content = Content.Folder.fromJson(jcontent.expectMap());
} else {
throw jcontent.unexpected();
}
return new Node(path, content);
}
}
public static abstract class Content
{
public abstract Object toJson();
public static final class Folder extends Content
{
public final HashMap<String,Node> children = new HashMap<String,Node>();
public JSONObject toJson()
{
JSONObject o = new JSONObject();
for (Map.Entry<String,Node> c : children.entrySet()) {
o.put(c.getKey(), c.getValue().toJson());
}
return o;
}
public static Folder fromJson(JsonMap j)
throws JsonExtractionException
{
Folder folder = new Folder();
for (Map.Entry<String,JsonThing> e : j) {
folder.children.put(e.getKey(), Node.fromJson(e.getValue()));
}
return folder;
}
}
public static final class File extends Content
{
public final String size;
public final String lastModified;
public final String clientMtime;
public File(String size, String lastModified, String clientMtime)
{
this.size = size;
this.lastModified = lastModified;
this.clientMtime = clientMtime;
}
public JSONArray toJson()
{
JSONArray j = new JSONArray();
j.add(size);
j.add(lastModified);
j.add(clientMtime);
return j;
}
public static File fromJson(JsonList l)
throws JsonExtractionException
{
return new File(l.get(0).expectString(), l.get(1).expectString(), l.get(2).expectString());
}
}
}
}