All Downloads are FREE. Search and download functionalities are using the official Maven repository.

com.dropbox.core.DbxClient Maven / Gradle / Ivy

The newest version!
package com.dropbox.core;

import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.util.ArrayList;

import com.dropbox.core.http.HttpRequestor;
import com.dropbox.core.json.JsonExtractionException;
import com.dropbox.core.json.JsonExtractor;
import com.dropbox.core.util.*;

/**
 * 

* Use this class to make remote calls to the Dropbox API. You'll need an access token first, * normally acquired via {@link DbxWebAuth}. *

* *

* This class has no mutable state, so it's thread safe as long as you pass in a thread safe * {@link HttpRequestor} implementation. *

*/ public final class DbxClient { private final DbxRequestConfig requestConfig; private final String accessToken; private final DbxHost host; /** * @param accessToken * The OAuth 2 access token (that you got from Dropbox) that gives your app the ability * to make Dropbox API calls against some particular user's account. The standard way * to get one of these is to use {@link DbxWebAuth} to send your user through Dropbox's * OAuth 2 authorization flow. */ public DbxClient(DbxRequestConfig requestConfig, String accessToken) { this(requestConfig, accessToken, DbxHost.Default); } /** * The same as {@link #DbxClient(DbxRequestConfig, String)} except you can also set the * hostnames of the Dropbox API servers. This is used in testing. You don't normally need * to call this. */ public DbxClient(DbxRequestConfig requestConfig, String accessToken, DbxHost host) { if (requestConfig == null) throw new IllegalArgumentException("'requestConfig' is null"); if (accessToken == null) throw new IllegalArgumentException("'accessToken' is null"); if (host == null) throw new IllegalArgumentException("'host' is null"); this.requestConfig = requestConfig; this.accessToken = accessToken; this.host = host; } /** * Returns the {@code DbxRequestConfig} that was passed in to the constructor. */ public DbxRequestConfig getRequestConfig() { return requestConfig; } /** * Returns the {@code DbxAccessToken} that was passed in to the constructor. */ public String getAccessToken() { return accessToken; } // ----------------------------------------------------------------- // /metadata /** * Get the file or folder metadata for a given path. * *
     * DbxClient dbxClient = ...
     * DbxEntry entry = dbxClient.getMetadata("/Photos");
     * if (entry == null) {
     *     System.out.println("No file or folder at that path.");
     * } else {
     *     System.out.print(entry.toStringMultiline());
     * }
     * 
* * @param path * The path to the file or folder (see {@link DbxPath}). * * @return If there is a file or folder at the given path, return the * metadata for that path. If there is no file or folder there, * return {@code null}. */ public DbxEntry getMetadata(final String path) throws DbxException { DbxPath.checkArg("path", path); String host = this.host.api; String apiPath = "1/metadata/auto" + path; String[] params = { "list", "false", }; return doGet(host, apiPath, params, null, new DbxRequestUtil.ResponseHandler() { @Override public DbxEntry handle(HttpRequestor.Response response) throws DbxException { if (response.statusCode == 404) return null; if (response.statusCode != 200) throw DbxRequestUtil.unexpectedStatus(response); // Will return 'null' for "is_deleted=true" entries. return DbxRequestUtil.extractJsonFromResponse(DbxEntry.Extractor, response.body); } }); } /** * Get the metadata for a given path; if the path refers to a folder, * get all the children's metadata as well. * *
     * DbxClient dbxClient = ...
     * DbxEntry entry = dbxClient.getMetadata("/Photos");
     * if (entry == null) {
     *     System.out.println("No file or folder at that path.");
     * } else {
     *     System.out.print(entry.toStringMultiline());
     * }
     * 
* * @param path * The path (starting with "/") to the file or folder (see {@link DbxPath}). * * @return If there is no file or folder at the given path, return {@code null}. * Otherwise, return the metadata for that path and the metadata for all its immediate * children (if it's a folder). */ public DbxEntry.WithChildren getMetadataWithChildren(String path) throws DbxException { return getMetadataWithChildrenBase(path, DbxEntry.WithChildren.Extractor); } /** * Same as {@link #getMetadataWithChildren} except instead of always returning a list of * {@link DbxEntry} objects, you specify a {@link Collector} that processes the {@link DbxEntry} * objects one by one and aggregates them however you want. * *

* This allows your to process the {@link DbxEntry} values as they arrive, instead of having to * wait for the entire API call to finish before processing the first one. Be careful, though, * because the API call may fail in the middle (after you've already processed some entries). * Make sure your code can handle that situation. For example, if you're inserting stuff into a * database as they arrive, you might want do everything in a transaction and commit only if * the entire call succeeds. *

*/ public DbxEntry.WithChildrenC getMetadataWithChildrenC(String path, final Collector collector) throws DbxException { return getMetadataWithChildrenBase(path, new DbxEntry.WithChildrenC.Extractor(collector)); } private T getMetadataWithChildrenBase(String path, final JsonExtractor extractor) throws DbxException { DbxPath.checkArg("path", path); String host = this.host.api; String apiPath = "1/metadata/auto" + path; String[] params = { "list", "true", "file_limit", "25000", }; return doGet(host, apiPath, params, null, new DbxRequestUtil.ResponseHandler() { @Override public T handle(HttpRequestor.Response response) throws DbxException { if (response.statusCode == 404) return null; if (response.statusCode != 200) throw DbxRequestUtil.unexpectedStatus(response); // Will return 'null' for "is_deleted=true" entries. return DbxRequestUtil.extractJsonFromResponse(extractor, response.body); } }); } /** * Get the metadata for a given path and its children if anything has * changed since the last time you got them (as determined by the value * of {@link DbxEntry.WithChildren#hash} from the last result). * * @param path * The path (starting with "/") to the file or folder (see {@link DbxPath}). * * @param previousFolderHash * The value of {@link DbxEntry.WithChildren#hash} from the last time * you got the metadata for this folder (and children). * * @return Never returns {@code null}. If the folder at the given path hasn't changed * since you last retrieved it (i.e. its contents match {@code previousFolderHash}), return * {@code Maybe.Nothing}. If it doesn't match {@code previousFolderHash} return either * {@code Maybe.Just(null)} if there's nothing there or {@code Maybe.Just} with the * metadata. */ public Maybe getMetadataWithChildrenIfChanged(String path, String previousFolderHash) throws DbxException { return getMetadataWithChildrenIfChangedBase(path, previousFolderHash, DbxEntry.WithChildren.Extractor); } /** * Same as {@link #getMetadataWithChildrenIfChanged} except instead of always returning a list of * {@link DbxEntry} objects, you specify a {@link Collector} that processes the {@link DbxEntry} * objects one by one and aggregates them however you want. * *

* This allows your to process the {@link DbxEntry} values as they arrive, instead of having to * wait for the entire API call to finish before processing the first one. Be careful, though, * because the API call may fail in the middle (after you've already processed some entries). * Make sure your code can handle that situation. For example, if you're inserting stuff into a * database as they arrive, you might want do everything in a transaction and commit only if * the entire call succeeds. *

*/ public Maybe> getMetadataWithChildrenIfChangedC( String path, String previousFolderHash, Collector collector) throws DbxException { return getMetadataWithChildrenIfChangedBase(path, previousFolderHash, new DbxEntry.WithChildrenC.Extractor(collector)); } private Maybe getMetadataWithChildrenIfChangedBase( String path, String previousFolderHash, final JsonExtractor extractor) throws DbxException { if (previousFolderHash == null) throw new IllegalArgumentException("'previousFolderHash' must not be null"); if (previousFolderHash.length() == 0) throw new IllegalArgumentException("'previousFolderHash' must not be empty"); DbxPath.checkArg("path", path); String host = this.host.api; String apiPath = "1/metadata/auto" + path; String[] params = { "list", "true", "file_limit", "25000", "hash", previousFolderHash, }; return doGet(host, apiPath, params, null, new DbxRequestUtil.ResponseHandler>() { @Override public Maybe handle(HttpRequestor.Response response) throws DbxException { if (response.statusCode == 404) return Maybe.Just(null); if (response.statusCode == 304) return Maybe.Nothing(); if (response.statusCode != 200) throw DbxRequestUtil.unexpectedStatus(response); return Maybe.Just(DbxRequestUtil.extractJsonFromResponse(extractor, response.body)); } }); } // ----------------------------------------------------------------- // /account_info /** * Retrieve the user's account information. */ public DbxAccountInfo getAccountInfo() throws DbxException { String host = this.host.api; String apiPath = "1/account/info"; return doGet(host, apiPath, null, null, new DbxRequestUtil.ResponseHandler() { @Override public DbxAccountInfo handle(HttpRequestor.Response response) throws DbxException { if (response.statusCode != 200) throw new DbxException.BadResponse("unexpected response code: " + response.statusCode); return DbxRequestUtil.extractJsonFromResponse(DbxAccountInfo.Extractor, response.body); } }); } // ----------------------------------------------------------------- // /files (GET) /** * Retrieves a file's content and writes it to the given {@code OutputStream}. * *
     * DbxClient dbxClient = ...
     * DbxEntry.File md;
     * File target = new File("Copy of House.jpeg");
     * OutputStream out = new FileOutputStream(target);
     * try {
     *     md = dbxClient.getFile("/Photos/House.jpeg", out);
     * }
     * finally {
     *     out.close();
     * }
     * 
* * @param revision * The {@link DbxEntry.File#revision }revision} of the file to retrieve, * or {@code null} if you want the latest revision of the file. * * @throws IOException * If there's an error writing to {@code target}. */ public DbxEntry.File getFile(String path, String revision, OutputStream target) throws DbxException, IOException { Downloader downloader = getFileStart(path, revision); try { IOUtil.copyStreamToStream(downloader.body, target); } catch (IOUtil.ReadException ex) { // Error reading from the network. Convert it to a DbxException. throw new DbxException.NetworkIO(ex.underlying); } catch (IOUtil.WriteException ex) { // Error writing to 'target'. Relay the underlying IOException. throw ex.underlying; } finally { downloader.close(); } return downloader.metadata; } /** * Retrieve a file's content and content metadata. Returns a {@link Downloader} * which is just an {@link InputStream} (can be used to read the file contents) and * a {@link DbxEntry.File} (the file's metadata). * *

* You need to close the {@link Downloader} yourself. * Use a {@code try}/{@code finally} to make sure you close it in all cases. *

* *
     * DbxClient dbxClient = ...
     * DbxClient.Downloader downloader = dbxClient.getFileStart("/ReadMe.txt")
     * try {
     *     printStream(downloader.body)
     * }
     * finally {
     *     downloader.close()
     * }
     * 
* * @param revision * The {@link DbxEntry.File#revision }revision} of the file to retrieve, * or {@code null} if you want the latest revision of the file. * * @param path * The path (starting with "/") to the file or folder on Dropbox. * (see {@link DbxPath}). */ public Downloader getFileStart(final String path, String revision) throws DbxException { DbxPath.checkArg("path", path); String host = this.host.content; String apiPath = "1/files/auto" + path; String[] params = new String[] { "rev", revision }; boolean passedOwnershipOfStream = false; HttpRequestor.Response response = DbxRequestUtil.startGet(requestConfig, accessToken, host, apiPath, params, null); try { if (response.statusCode == 404) return null; if (response.statusCode != 200) throw DbxRequestUtil.unexpectedStatus(response); String metadataString = DbxRequestUtil.getFirstHeader(response, "x-dropbox-metadata"); DbxEntry metadata; try { metadata = DbxEntry.Extractor.extractFull(metadataString); } catch (JsonExtractionException ex) { throw new DbxException.BadResponse("Bad JSON in X-Dropbox-Metadata header: " + ex.getMessage(), ex); } if (metadata instanceof DbxEntry.Folder) { throw new DbxException.BadResponse("downloaded file, but server returned metadata entry for a folder"); } DbxEntry.File fileMetadata = (DbxEntry.File) metadata; // Package up the metadata with the response body's InputStream. Downloader result = new Downloader(fileMetadata, response.body); passedOwnershipOfStream = true; return result; } finally { // If we haven't passed ownership the stream to the caller, then close it. if (!passedOwnershipOfStream) { try { response.body.close(); } catch (IOException ex) { // Ignore, since we don't actually care about the data in this method. // We only care about IOExceptions when actually reading from the stream. } } } } /** * The result of a {@link #getFileStart DbClient.getFile}. Just a pairing * of the file content's metadata and an {@link InputStream} to read the file content. * Make sure you always close the {@code InputStream}. */ public static final class Downloader { public final DbxEntry.File metadata; public final InputStream body; public Downloader(DbxEntry.File metadata, InputStream body) { this.metadata = metadata; this.body = body; } public void close() { try { body.close(); } catch (IOException ex) { // Don't care about IOExceptions on InputStreams because // we've already read what we wanted. } } } // -------------------------------------------------------- // /files_put // TODO: Maybe switch to /files (POST) so we won't accidentally upload partial // files (multi-part form encoding will ensure that the server knows when the // upload is actually complete). /** * A wrapper around {@link #uploadFile(String, DbxWriteMode, long, DbxStreamWriter)} that * lets you pass in an {@link InputStream}. The entire stream {@code contents} will * be uploaded and the stream will be closed automatically (whether or not the upload * succeeds or fails). * *
     * DbxClient dbxClient = ...
     * File f = new File("ReadMe.txt")
     * dbxClient.uploadFile("/ReadMe.txt", {@link DbxWriteMode#add()}, f.length(), new FileInputStream(f))
     * 
* * @param targetPath * The path to the file on Dropbox (see {@link DbxPath}). If a file at * that path already exists on Dropbox, then the {@code writeMode} parameter * will determine what happens. * * @param writeMode * Determines what to do if there's already a file at the given {@code targetPath}. * * @param numBytes * The number of bytes in the given stream. Use {@code -1} if you don't know. * * @param contents * The source of file contents. This stream will be automatically closed (whether or not the * upload succeeds). * * @throws IOException * If there's an error reading from {@code in}. */ public DbxEntry.File uploadFile(String targetPath, DbxWriteMode writeMode, long numBytes, InputStream contents) throws DbxException, IOException { DbxPath.checkArg("targetPath", targetPath); return uploadFile(targetPath, writeMode, numBytes, new DbxStreamWriter.InputStreamCopier(contents)); } /** * Upload file contents to Dropbox, getting contents from the given {@link DbxStreamWriter}. * *
     * DbxClient dbxClient = ...
     * // Create a file on Dropbox with 100 3-digit random numbers, one per line.
     * final int numRandoms = 100;
     * int fileSize = numRandoms * 4;  3 digits, plus a newline
     * dbxClient.uploadFile("/Randoms.txt", {@link DbxWriteMode#add()}, fileSize,
     *     new DbxStreamWriter<RuntimeException>() {
     *         public void write(OutputStream out) throws IOException
     *         {
     *             Random rand = new Random();
     *             PrintWriter pw = new PrintWriter(out);
     *             for (int i = 0; i < numRandoms; i++) {
     *                 pw.printf("%03d\n", rand.nextInt(1000)
     *             }
     *             pw.flush();
     *         }
     *     });
     * 
* * @param targetPath * The path to the file on Dropbox (see {@link DbxPath}). If a file at * that path already exists on Dropbox, then the {@code writeMode} parameter * will determine what happens. * * @param writeMode * Determines what to do if there's already a file at the given {@code targetPath}. * * @param numBytes * The number of bytes you're going to upload via the returned {@link DbxClient.Uploader}. * Use {@code -1} if you don't know ahead of time. * * @param writer * A callback that will be called when it's time to actually write out the * body of the file. We will always call {@link DbxStreamWriter#close} on it (whether * or not the upload succeeds) so you can put resource cleanup code in it (for example, * closing an {@code InputStream}). * * @throws E * If {@code writer.write()} throws an exception, it will propagate out of this function. */ public DbxEntry.File uploadFile(String targetPath, DbxWriteMode writeMode, long numBytes, DbxStreamWriter writer) throws DbxException, E { DbxPath.checkArg("targetPath", targetPath); try { Uploader uploader = startUploadFile(targetPath, writeMode, numBytes); NoThrowOutputStream streamWrapper = new NoThrowOutputStream(uploader.body); try { writer.write(streamWrapper); return uploader.finish(); } catch (NoThrowOutputStream.HiddenException ex) { // We hid our OutputStream's IOException from their writer.write() function so that // we could properly raise a NetworkIO exception if something went wrong with the // network stream. throw new DbxException.NetworkIO(ex.underlying); } finally { uploader.close(); } } finally { writer.close(); } } /** * Start an API request to upload a file to Dropbox. Returns a {@link DbxClient.Uploader} object * that lets you actually send the file contents via {@link DbxClient.Uploader#body}. When * you're done copying the file body, call {@link DbxClient.Uploader#finish}. * *

* You need to close the {@link Uploader} when you're done with it. * Use a {@code try}/{@code finally} to make sure you close it in all cases. *

* *
     * DbxClient dbxClient = ...
     * DbxClient.Uploader uploader = dbxClient.startUploadFile(...)
     * DbxEntry.File md;
     * try {
     *     writeMyData(uploader.body);
     *     md = uploader.finish();
     * }
     * finally {
     *     uploader.close();
     * }
     * 
* * @param targetPath * The path to the file on Dropbox (see {@link DbxPath}). If a file at * that path already exists on Dropbox, then the {@code writeMode} parameter * will determine what happens. * * @param writeMode * Determines what to do if there's already a file at the given {@code targetPath}. * * @param numBytes * The number of bytes you're going to upload via the returned {@link DbxClient.Uploader}. * Use {@code -1} if you don't know ahead of time. */ public Uploader startUploadFile(String targetPath, DbxWriteMode writeMode, long numBytes) throws DbxException { DbxPath.checkArg("targetPath", targetPath); String host = this.host.content; String apiPath = "1/files_put/auto" + targetPath; HttpRequestor.Uploader uploader = DbxRequestUtil.startPut(requestConfig, accessToken, host, apiPath, writeMode.params, null); return new Uploader(uploader, numBytes); } // ----------------------------------------------------------------- // /delta /** * Return "delta" entries for the contents of a user's Dropbox. This lets you * efficiently keep up with the latest state of the files and folders. See * {@link DbxDelta} for more documentation on what each entry means. * *

* To start, pass in {@code null} for {@code cursor}. For subsequent calls * To get the next set of delta entries, pass in the {@link DbxDelta#cursor cursor} returned * by the previous call. *

* *

* To catch up to the current state, keep calling this method until the returned * object's {@link DbxDelta#hasMore hasMore} field is {@code false}. *

* *

* If your app is a "Full Dropbox" app, this will return all entries for the user's entire * Dropbox folder. If your app is an "App Folder" app, this will only return entries * for the App Folder's contents. *

*/ public DbxDelta getDelta(String cursor) throws DbxException { String host = this.host.api; String apiPath = "1/delta"; String[] params = {"cursor", cursor}; return DbxRequestUtil.doPost(requestConfig, accessToken, host, apiPath, params, null, new DbxRequestUtil.ResponseHandler>() { @Override public DbxDelta handle(HttpRequestor.Response response) throws DbxException { if (response.statusCode != 200) throw DbxRequestUtil.unexpectedStatus(response); return DbxRequestUtil.extractJsonFromResponse(new DbxDelta.Extractor(DbxEntry.Extractor), response.body); } }); } /** * This is a more generic version of {@link #getDelta}. It allows you to specify * a collector, which lets you process the delta entries as they arrive over * the network, and aggregate them however you want. */ public DbxDeltaC getDeltaC(String cursor, final Collector, C> collector) throws DbxException { String host = this.host.api; String apiPath = "1/delta"; return DbxRequestUtil.doPost(requestConfig, accessToken, host, apiPath, new String[]{"cursor", cursor}, null, new DbxRequestUtil.ResponseHandler>() { @Override public DbxDeltaC handle(HttpRequestor.Response response) throws DbxException { if (response.statusCode != 200) throw DbxRequestUtil.unexpectedStatus(response); return DbxRequestUtil.extractJsonFromResponse(new DbxDeltaC.Extractor(DbxEntry.Extractor, collector), response.body); } }); } // -------------------------------------------------------- // Convenience function that calls Util.doGet with the first three parameters filled in. private T doGet(String host, String path, String[] params, ArrayList headers, DbxRequestUtil.ResponseHandler handler) throws DbxException { return DbxRequestUtil.doGet(requestConfig, accessToken, host, path, params, headers, handler); } /** * For uploading file content to Dropbox. Write stuff to the {@link #body} stream. * *

* Don't call {@code close()} directly on the {@link #body}. Instead call either * call either {@link #finish} or {@link #close} to make sure the stream and other * resources are released. A safe idiom is to use the object within a {@code try} * block and put a call to {@link #close()} in the {@code finally} block. *

* *
     * DbxClient.Uploader uploader = ...
     * try {
     *     uploader.body.write("Hello, world!".getBytes("UTF-8"));
     *     uploader.finish();
     * }
     * finally {
     *     uploader.close();
     * }
     * 
*/ public static final class Uploader { private HttpRequestor.Uploader httpUploader; private final long claimedBytes; private final CountingOutputStream countingStream; public final OutputStream body; public Uploader(HttpRequestor.Uploader httpUploader, long claimedBytes) { if (claimedBytes < 0) { throw new IllegalArgumentException("'numBytes' must be greater than or equal to 0"); } this.httpUploader = httpUploader; this.claimedBytes = claimedBytes; this.countingStream = new CountingOutputStream(httpUploader.body); this.body = countingStream; } /** * Cancel the upload. */ public void abort() { if (httpUploader == null) { throw new IllegalStateException("already called 'finish', 'abort', or 'close'"); } HttpRequestor.Uploader p = httpUploader; httpUploader = null; p.abort(); } /** * Release the resources related to this {@code Uploader} instance. If * {@link #close()} or {@link #abort()} has already been called, this does nothing. * If neither has been called, this is equivalent to calling {@link #abort()}. */ public void close() { // If already close'd or aborted, then don't do anything. if (httpUploader == null) return; abort(); } /** * When you're done writing the file contents to {@link #body}, call this * to indicate that you're done. This will actually finish the underlying HTTP * request and return the uploaded file's {@link DbxEntry}. */ public DbxEntry.File finish() throws DbxException { if (httpUploader == null) { throw new IllegalStateException("already called 'finish', 'abort', or 'close'"); } HttpRequestor.Uploader u = httpUploader; httpUploader = null; long bytesWritten = this.countingStream.getBytesWritten(); if (claimedBytes != bytesWritten) { // Make sure the uploaded the same number of bytes they said they were going to upload. throw new IllegalStateException("You said you were going to upload " + claimedBytes + " bytes, but you wrote " + bytesWritten + " bytes to the Uploader's 'body' stream."); } HttpRequestor.Response response; try { response = u.finish(); } catch (IOException ex) { throw new DbxException.NetworkIO(ex); } if (response.statusCode != 200) throw DbxRequestUtil.unexpectedStatus(response); DbxEntry entry = DbxRequestUtil.extractJsonFromResponse(DbxEntry.Extractor, response.body); if (entry instanceof DbxEntry.Folder) { throw new DbxException.BadResponse("uploaded file, but server returned metadata entry for a folder"); } DbxEntry.File f = (DbxEntry.File) entry; // Make sure the server agrees with us on how many bytes are in the file. if (f.numBytes != bytesWritten) { throw new DbxException.BadResponse("we uploaded " + bytesWritten + ", but server returned metadata entry with file size " + f.numBytes); } return f; } } }




© 2015 - 2025 Weber Informatics LLC | Privacy Policy