/* * Copyright (c) 2009-2011 Dropbox, Inc. * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal * in the Software without restriction, including without limitation the rights * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE * SOFTWARE. */ package com.dropbox.client2; import java.io.BufferedOutputStream; import java.io.FileOutputStream; import java.io.FilterInputStream; import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; import java.io.Serializable; import java.io.SyncFailedException; import java.util.ArrayList; import java.util.Date; import java.util.Iterator; import java.util.LinkedList; import java.util.List; import java.util.Map; import org.apache.http.Header; import org.apache.http.HttpEntity; import org.apache.http.HttpResponse; import org.apache.http.client.methods.HttpGet; import org.apache.http.client.methods.HttpPut; import org.apache.http.client.methods.HttpUriRequest; import org.apache.http.entity.InputStreamEntity; import org.json.simple.JSONArray; import org.json.simple.JSONValue; import com.dropbox.client2.ProgressListener.ProgressHttpEntity; import com.dropbox.client2.RESTUtility.RequestMethod; import com.dropbox.client2.exception.DropboxException; import com.dropbox.client2.exception.DropboxFileSizeException; import com.dropbox.client2.exception.DropboxIOException; import com.dropbox.client2.exception.DropboxLocalStorageFullException; import com.dropbox.client2.exception.DropboxParseException; import com.dropbox.client2.exception.DropboxPartialFileException; import com.dropbox.client2.exception.DropboxServerException; import com.dropbox.client2.exception.DropboxUnlinkedException; import com.dropbox.client2.jsonextract.JsonExtractionException; import com.dropbox.client2.jsonextract.JsonExtractor; import com.dropbox.client2.jsonextract.JsonList; import com.dropbox.client2.jsonextract.JsonMap; import com.dropbox.client2.jsonextract.JsonThing; import com.dropbox.client2.session.Session; /** * Location of the Dropbox API functions. * * The class is parameterized with the type of session it uses. This will be * the same as the type of session you pass into the constructor. */ public class DropboxAPI<SESS_T extends Session> { /** * The version of the API that this code uses. */ public static final int VERSION = 1; /** * The version of this Dropbox SDK. */ public static final String SDK_VERSION = SdkVersion.get(); /** * The max upload file size that Dropbox servers can handle, in bytes. */ public static final long MAX_UPLOAD_SIZE = 180 * 1024 * 1024; // 180MiB protected static final int METADATA_DEFAULT_LIMIT = 25000; private static final int REVISION_DEFAULT_LIMIT = 1000; private static final int SEARCH_DEFAULT_LIMIT = 10000; private static final int UPLOAD_SO_TIMEOUT_MS = 3 * 60 * 1000; // 3 minutes protected final SESS_T session; public DropboxAPI(SESS_T session) { if (session == null) { throw new IllegalArgumentException("Session must not be null."); } this.session = session; } /** * Information about a user's account. */ public static class Account implements Serializable { private static final long serialVersionUID = 2097522622341535732L; /** The user's ISO country code. */ public final String country; /** The user's "real" name. */ public final String displayName; /** The user's quota, in bytes. */ public final long quota; /** The user's quota excluding shared files. */ public final long quotaNormal; /** The user's quota of shared files. */ public final long quotaShared; /** The user's account ID. */ public final long uid; /** The url the user can give to get referral credit. */ public final String referralLink; /** * Creates an account from a Map. * * @param map a Map that looks like: * <pre> * {"country": "", * "display_name": "John Q. User", * "quota_info": { * "shared": 37378890, * "quota": 62277025792, * "normal": 263758550 * }, * "uid": "174"} * </pre> */ protected Account(Map<String, Object> map) { country = (String) map.get("country"); displayName = (String) map.get("display_name"); uid = getFromMapAsLong(map, "uid"); referralLink = (String) map.get("referral_link"); Object quotaInfo = map.get("quota_info"); @SuppressWarnings("unchecked") Map<String, Object> quotamap = (Map<String, Object>) quotaInfo; quota = getFromMapAsLong(quotamap, "quota"); quotaNormal = getFromMapAsLong(quotamap, "normal"); quotaShared = getFromMapAsLong(quotamap, "shared"); } /** * Creates an account object from an initial set of values. */ protected Account(String country, String displayName, long uid, String referralLink, long quota, long quotaNormal, long quotaShared) { this.country = country; this.displayName = displayName; this.uid = uid; this.referralLink = referralLink; this.quota = quota; this.quotaNormal = quotaNormal; this.quotaShared = quotaShared; } } /** * A metadata entry that describes a file or folder. */ public static class Entry { /** Size of the file. */ public long bytes; /** * If a directory, the hash is its "current version". If the hash * changes between calls, then one of the directory's immediate * children has changed. */ public String hash; /** * Name of the icon to display for this entry. Corresponds to filenames * (without an extension) in the icon library available at * https://www.dropbox.com/static/images/dropbox-api-icons.zip. */ public String icon; /** True if this entry is a directory, or false if it's a file. */ public boolean isDir; /** * Last modified date, in "EEE, dd MMM yyyy kk:mm:ss ZZZZZ" form (see * {@code RESTUtility#parseDate(String)} for parsing this value. */ public String modified; /** * For a file, this is the modification time set by the client when * the file was added to Dropbox. Since this time is not verified (the * Dropbox server stores whatever the client sends up) this should only * be used for display purposes (such as sorting) and not, for example, * to determine if a file has changed or not. * * <p> * This is not set for folders. * </p> */ public String clientMtime; /** Path to the file from the root. */ public String path; /** * Name of the root, usually either "dropbox" or "app_folder". */ public String root; /** * Human-readable (and localized, if possible) description of the * file size. */ public String size; /** The file's MIME type. */ public String mimeType; /** * Full unique ID for this file's revision. This is a string, and not * equivalent to the old revision integer. */ public String rev; /** Whether a thumbnail for this is available. */ public boolean thumbExists; /** * Whether this entry has been deleted but not removed from the * metadata yet. Most likely you'll only want to show entries with * isDeleted == false. */ public boolean isDeleted; /** A list of immediate children if this is a directory. */ public List<Entry> contents; /** * Creates an entry from a map, usually received from the metadata * call. It's unlikely you'll want to create these yourself. * * @param map the map representation of the JSON received from the * metadata call, which should look like this: * <pre> * { * "hash": "528dda36e3150ba28040052bbf1bfbd1", * "thumb_exists": false, * "bytes": 0, * "modified": "Sat, 12 Jan 2008 23:10:10 +0000", * "path": "/Public", * "is_dir": true, * "size": "0 bytes", * "root": "dropbox", * "contents": [ * { * "thumb_exists": false, * "bytes": 0, * "modified": "Wed, 16 Jan 2008 09:11:59 +0000", * "path": "/Public/\u2665asdas\u2665", * "is_dir": true, * "icon": "folder", * "size": "0 bytes" * }, * { * "thumb_exists": false, * "bytes": 4392763, * "modified": "Thu, 15 Jan 2009 02:52:43 +0000", * "path": "/Public/\u540d\u79f0\u672a\u8a2d\u5b9a\u30d5\u30a9\u30eb\u30c0.zip", * "is_dir": false, * "icon": "page_white_compressed", * "size": "4.2MB" * } * ], * "icon": "folder_public" * } * </pre> */ @SuppressWarnings("unchecked") public Entry(Map<String, Object> map) { bytes = getFromMapAsLong(map, "bytes"); hash = (String) map.get("hash"); icon = (String) map.get("icon"); isDir = getFromMapAsBoolean(map, "is_dir"); modified = (String) map.get("modified"); clientMtime = (String) map.get("client_mtime"); path = (String) map.get("path"); root = (String) map.get("root"); size = (String) map.get("size"); mimeType = (String) map.get("mime_type"); rev = (String) map.get("rev"); thumbExists = getFromMapAsBoolean(map, "thumb_exists"); isDeleted = getFromMapAsBoolean(map, "is_deleted"); Object json_contents = map.get("contents"); if (json_contents != null && json_contents instanceof JSONArray) { contents = new ArrayList<Entry>(); Object entry; Iterator<?> it = ((JSONArray) json_contents).iterator(); while (it.hasNext()) { entry = it.next(); if (entry instanceof Map) { contents.add(new Entry((Map<String, Object>) entry)); } } } else { contents = null; } } public Entry() { } /** * Returns the file name if this is a file (the part after the last * slash in the path). */ public String fileName() { int ind = path.lastIndexOf('/'); return path.substring(ind + 1, path.length()); } /** * Returns the path of the parent directory if this is a file. */ public String parentPath() { if (path.equals("/")) { return ""; } else { int ind = path.lastIndexOf('/'); return path.substring(0, ind + 1); } } public static final JsonExtractor<Entry> JsonExtractor = new JsonExtractor<Entry>() { public Entry extract(JsonThing jt) throws JsonExtractionException { return new Entry(jt.expectMap().internal); } }; } /** * Contains info describing a downloaded file. */ public static final class DropboxFileInfo { private String mimeType = null; private long fileSize = -1; private String charset = null; private Entry metadata = null; // fileSize and metadata are guaranteed to be valid if the constructor // doesn't throw an exception. private DropboxFileInfo(HttpResponse response) throws DropboxException { metadata = parseXDropboxMetadata(response); if (metadata == null) { throw new DropboxParseException("Error parsing metadata."); } fileSize = parseFileSize(response, metadata); if (fileSize == -1) { throw new DropboxParseException("Error determining file size."); } // Parse mime type and charset. Header contentType = response.getFirstHeader("Content-Type"); if (contentType != null) { String contentVal = contentType.getValue(); if (contentVal != null) { String[] splits = contentVal.split(";"); if (splits.length > 0) { mimeType = splits[0].trim(); } if (splits.length > 1) { splits = splits[1].split("="); if (splits.length > 1) { charset = splits[1].trim(); } } } } } /** * Parses the JSON in the the 'x-dropbox-metadata' header field of the * http response. * * @param response The http response for the downloaded file. * @return An Entry object based on the metadata JSON. Can be null if * metadata isn't available. */ private static Entry parseXDropboxMetadata(HttpResponse response) { if (response == null) { return null; } Header xDropboxMetadataHeader = response.getFirstHeader("X-Dropbox-Metadata"); if (xDropboxMetadataHeader == null) { return null; } // Returns null if the parsing fails. String json = xDropboxMetadataHeader.getValue(); Object metadata = JSONValue.parse(json); if (metadata == null) { return null; } @SuppressWarnings("unchecked") Map<String,Object> map = (Map<String,Object>) metadata; return new Entry(map); } /** * Determines the size of the downloaded file. * * @param response The http response for the file whose size we're * interested in. * @param metadata The metadata associated with the file. Can be null if * unavailable. * @return The determined file size. -1 if the size of the file can't be * determined. */ private static long parseFileSize(HttpResponse response, Entry metadata) { // Use the response's content-length, if available (negative if // unavailable). long contentLength = response.getEntity().getContentLength(); if (contentLength >= 0) { return contentLength; } // Fall back on the metadata, if available. if (metadata != null) { return metadata.bytes; } return -1; } /** * Returns the MIME type of the associated file, or null if it is * unknown. */ public final String getMimeType() { return mimeType; } /** * @deprecated Replaced by {@link #getFileSize()} */ @Deprecated public final long getContentLength() { return getFileSize(); } /** * Returns the size of the file in bytes (always >= 0). */ public final long getFileSize() { return fileSize; } /** * Returns the charset of the associated file, or null if it is * unknown. */ public final String getCharset() { return charset; } /** * Returns the metadata of the associated file (always non-null). */ public final Entry getMetadata() { return metadata; } } /** * An {@link InputStream} for a file download that includes the associated * {@link DropboxFileInfo}. Closing this stream will cancel the associated * download request. */ public static class DropboxInputStream extends FilterInputStream { private final HttpUriRequest request; private final DropboxFileInfo info; public DropboxInputStream(HttpUriRequest request, HttpResponse response) throws DropboxException { // Give the FilterInputStream a null stream at first so we can // handle errors better. super(null); HttpEntity entity = response.getEntity(); if (entity == null) { throw new DropboxException("Didn't get entity from HttpResponse"); } // Now set the input stream on FilterInputStream. This will throw // an IOException itself if something goes wrong. try { in = entity.getContent(); } catch (IOException e) { throw new DropboxIOException(e); } this.request = request; info = new DropboxFileInfo(response); } /** * Closes this stream and aborts the request to Dropbox, releasing * any associated resources. No more bytes will be downloaded after * this is called. * * @throws IOException if an error occurs while closing this stream. */ @Override public void close() throws IOException { // Aborting the request also closes the input stream that it // creates (the in variable). Do not try to close it again. request.abort(); } /** * Returns the {@link DropboxFileInfo} for the associated file. */ public DropboxFileInfo getFileInfo() { return info; } /** * Copies from a {@link DropboxInputStream} to an * {@link OutputStream}, optionally providing updates via a * {@link ProgressListener}. You probably won't have a use for this * function because most API functions that return a * {@link DropboxInputStream} have an alternate that will copy to an * {@link OutputStream} for you. * * @param os the stream to copy to. * @param listener an optional {@link ProgressListener} to receive progress * updates as the stream is copied, or null. * * @throws DropboxPartialFileException if only part of the input stream was * copied. * @throws DropboxIOException for network-related errors. * @throws DropboxLocalStorageFullException if there is no more room to * write to the output stream. * @throws DropboxException for any other unknown errors. This is also a * superclass of all other Dropbox exceptions, so you may want to * only catch this exception which signals that some kind of error * occurred. */ public void copyStreamToOutput(OutputStream os, ProgressListener listener) throws DropboxIOException, DropboxPartialFileException, DropboxLocalStorageFullException { BufferedOutputStream bos = null; long totalRead = 0; long lastListened = 0; long length = info.getFileSize(); try { bos = new BufferedOutputStream(os); byte[] buffer = new byte[4096]; int read; while (true) { read = read(buffer); if (read < 0) { if (length >= 0 && totalRead < length) { // We've reached the end of the file, but it's unexpected. throw new DropboxPartialFileException(totalRead); } // TODO check for partial success, if possible break; } bos.write(buffer, 0, read); totalRead += read; if (listener != null) { long now = System.currentTimeMillis(); if (now - lastListened > listener.progressInterval()) { lastListened = now; listener.onProgress(totalRead, length); } } } bos.flush(); os.flush(); // Make sure it's flushed out to disk try { if (os instanceof FileOutputStream) { ((FileOutputStream)os).getFD().sync(); } } catch (SyncFailedException e) { } } catch (IOException e) { String message = e.getMessage(); if (message != null && message.startsWith("No space")) { // This is a hack, but it seems to be the only way to check // which exception it is. throw new DropboxLocalStorageFullException(); } else { /* * If the output stream was closed, we notify the caller * that only part of the file was copied. This could have * been because this request is being intentionally * canceled. */ throw new DropboxPartialFileException(totalRead); } } finally { if (bos != null) { try { bos.close(); } catch (IOException e) {} } if (os != null) { try { os.close(); } catch (IOException e) {} } // This will also abort/finish the request if the download is // canceled early. try { close(); } catch (IOException e) {} } } } /** * A request to upload a file to Dropbox. This request can be canceled * by calling abort(). */ public interface UploadRequest { /** * Aborts the request. The original call to upload() will throw a * {@link DropboxPartialFileException}. */ public void abort(); /** * Executes the request. * * @return an {@link Entry} representing the uploaded file. * * @throws DropboxPartialFileException if the request was canceled * before completion. * @throws DropboxServerException if the server responds with an error * code. See the constants in {@link DropboxServerException} for * the meaning of each error code. The most common error codes * you can expect from this call are 404 (path to upload not * found), 507 (user over quota), and 400 (unexpected parent * rev). * @throws DropboxIOException if any network-related error occurs. * @throws DropboxException for any other unknown errors. This is also a * superclass of all other Dropbox exceptions, so you may want to * only catch this exception which signals that some kind of error * occurred. */ public Entry upload() throws DropboxException; } protected static final class BasicUploadRequest implements UploadRequest { private final HttpUriRequest request; private final Session session; public BasicUploadRequest(HttpUriRequest request, Session session) { this.request = request; this.session = session; } /** * Aborts the request. The original call to upload() will throw a * {@link DropboxPartialFileException}. */ @Override public void abort() { request.abort(); } /** * Executes the request. * * @return an {@link Entry} representing the uploaded file. * * @throws DropboxPartialFileException if the request was canceled * before completion. * @throws DropboxServerException if the server responds with an error * code. See the constants in {@link DropboxServerException} for * the meaning of each error code. The most common error codes * you can expect from this call are 404 (path to upload not * found), 507 (user over quota), and 400 (unexpected parent * rev). * @throws DropboxIOException if any network-related error occurs. * @throws DropboxException for any other unknown errors. This is also a * superclass of all other Dropbox exceptions, so you may want to * only catch this exception which signals that some kind of error * occurred. */ @Override public Entry upload() throws DropboxException { HttpResponse hresp; try { hresp = RESTUtility.execute(session, request, UPLOAD_SO_TIMEOUT_MS); } catch (DropboxIOException e) { if (request.isAborted()) { throw new DropboxPartialFileException(-1); } else { throw e; } } Object resp = RESTUtility.parseAsJSON(hresp); @SuppressWarnings("unchecked") Map<String, Object> ret = (Map<String, Object>) resp; return new Entry(ret); } } /** * Holds an {@link HttpUriRequest} and the associated {@link HttpResponse}. */ public static final class RequestAndResponse { /** The request */ public final HttpUriRequest request; /** The response */ public final HttpResponse response; protected RequestAndResponse(HttpUriRequest request, HttpResponse response) { this.request = request; this.response = response; } } /** * Contains a link to a Dropbox stream or share and its expiration date. */ public static class DropboxLink { /** The url it links to */ public final String url; /** When the url expires (after which this link will no longer work) */ public final Date expires; private DropboxLink(String returl, boolean secure) { if (!secure && returl.startsWith("https://")) { returl = returl.replaceFirst("https://", "http://"); returl = returl.replaceFirst(":443/", "/"); } url = returl; expires = null; } private DropboxLink(Map<String, Object> map) { this(map, true); } /** * Creates a DropboxLink, with security optionally set to false. * This is useful for some clients, such as Android, which use * this to play a streaming audio or video file, and which are * unable to play from streaming https links. * * @param map the parsed parameters returned from Dropbox * @param secure if false, returns an http link */ private DropboxLink(Map<String, Object> map, boolean secure) { String returl = (String)map.get("url"); String exp = (String)map.get("expires"); if (exp != null) { expires = RESTUtility.parseDate(exp); } else { expires = null; } if (!secure && returl.startsWith("https://")) { returl = returl.replaceFirst("https://", "http://"); returl = returl.replaceFirst(":443/", "/"); } url = returl; } } /** * Represents the size of thumbnails that the API can return. */ public enum ThumbSize { /** * Will have at most a 32 width or 32 height, maintaining its * original aspect ratio. */ ICON_32x32("small"), /** 64 width or 64 height, with original aspect ratio. */ ICON_64x64("medium"), /** 128 width or 128 height, with original aspect ratio. */ ICON_128x128("large"), /** 256 width or 256 height, with original aspect ratio. */ ICON_256x256("256x256"), /** * Will either fit within a 320 x 240 rectangle or a * 240 x 320 rectangle, whichever results in a larger image. */ BESTFIT_320x240("320x240_bestfit"), /** Fits within 480x320 or 320x480 */ BESTFIT_480x320("480x320_bestfit"), /** Fits within 640x480 or 480x640 */ BESTFIT_640x480("640x480_bestfit"), /** Fits within 960x640 or 640x960 */ BESTFIT_960x640("960x640_bestfit"), /** Fits within 1024x768 or 768x1024 */ BESTFIT_1024x768("1024x768_bestfit"); private String size; ThumbSize(String size) { this.size = size; } public String toAPISize() { return size; } } /** * Represents the image format of thumbnails that the API can return. */ public enum ThumbFormat { PNG, JPEG } /** * Returns the {@link Session} that this API is using. */ public SESS_T getSession() { return session; } /** * Returns the {@link Account} associated with the current {@link Session}. * * @return the current session's {@link Account}. * * @throws DropboxUnlinkedException if you have not set an access token * pair on the session, or if the user has revoked access. * @throws DropboxServerException if the server responds with an error * code. See the constants in {@link DropboxServerException} for * the meaning of each error code. * @throws DropboxIOException if any network-related error occurs. * @throws DropboxException for any other unknown errors. This is also a * superclass of all other Dropbox exceptions, so you may want to * only catch this exception which signals that some kind of error * occurred. */ public Account accountInfo() throws DropboxException { assertAuthenticated(); @SuppressWarnings("unchecked") Map<String, Object> accountInfo = (Map<String, Object>) RESTUtility.request(RequestMethod.GET, session.getAPIServer(), "/account/info", VERSION, new String[] {"locale", session.getLocale().toString()}, session); return new Account(accountInfo); } /** * Downloads a file from Dropbox, copying it to the output stream. Returns * the {@link DropboxFileInfo} for the file. * * @param path the Dropbox path to the file. * @param rev the revision (from the file's metadata) of the file to * download, or null to get the latest version. * @param os the {@link OutputStream} to write the file to. * @param listener an optional {@link ProgressListener} to receive progress * updates as the file downloads, or null. * * @return the {@link DropboxFileInfo} for the downloaded file. * * @throws DropboxUnlinkedException if you have not set an access token * pair on the session, or if the user has revoked access. * @throws DropboxServerException if the server responds with an error * code. See the constants in {@link DropboxServerException} for * the meaning of each error code. The most common error codes you * can expect from this call are 404 (path not found) and 400 (bad * rev). * @throws DropboxPartialFileException if a network error occurs during the * download. * @throws DropboxIOException for some network-related errors. * @throws DropboxException for any other unknown errors. This is also a * superclass of all other Dropbox exceptions, so you may want to * only catch this exception which signals that some kind of error * occurred. */ public DropboxFileInfo getFile(String path, String rev, OutputStream os, ProgressListener listener) throws DropboxException { DropboxInputStream dis = getFileStream(path, rev); dis.copyStreamToOutput(os, listener); return dis.getFileInfo(); } /** * Downloads a file from Dropbox. Returns a {@link DropboxInputStream} via * which the file contents can be read from the network. You must close the * stream when you're done with it to release all resources. * * You can also cancel the download by closing the returned * {@link DropboxInputStream} at any time. * * @param path the Dropbox path to the file. * @param rev the revision (from the file's metadata) of the file to * download, or null to get the latest version. * * @return a {@link DropboxInputStream} from which to read the file * contents. The contents are retrieved from the network and not * stored locally. * * @throws DropboxUnlinkedException if you have not set an access token * pair on the session, or if the user has revoked access. * @throws DropboxServerException if the server responds with an error * code. See the constants in {@link DropboxServerException} for * the meaning of each error code. The most common error codes you * can expect from this call are 404 (path not found) and 400 (bad * rev). * @throws DropboxIOException if any network-related error occurs. * @throws DropboxException for any other unknown errors. This is also a * superclass of all other Dropbox exceptions, so you may want to * only catch this exception which signals that some kind of error * occurred. */ public DropboxInputStream getFileStream(String path, String rev) throws DropboxException { assertAuthenticated(); if (!path.startsWith("/")) { path = "/" + path; } String url = "/files/" + session.getAccessType() + path; String[] args = new String[] { "rev", rev, "locale", session.getLocale().toString(), }; String target = RESTUtility.buildURL(session.getContentServer(), VERSION, url, args); HttpGet req = new HttpGet(target); session.sign(req); HttpResponse response = RESTUtility.execute(session, req); return new DropboxInputStream(req, response); } /** * Uploads a file to Dropbox. The upload will not overwrite any existing * version of the file, unless the latest version on the Dropbox server * has the same rev as the parentRev given. Pass in null if you're expecting * this to create a new file. Note: use {@code putFileRequest()} if you want * to be able to cancel the upload. * * @param path the full Dropbox path where to put the file, including * directories and filename. * @param is the {@link InputStream} from which to upload. * @param length the amount of bytes to read from the {@link InputStream}. * @param parentRev the rev of the file at which the user started editing * it (obtained from a metadata call), or null if this is a new * upload. If null, or if it does not match the latest rev on the * server, a copy of the file will be created and you'll receive * the new metadata upon executing the request. * @param listener an optional {@link ProgressListener} to receive upload * progress updates, or null. * * @return a metadata {@link Entry} representing the uploaded file. * * @throws IllegalArgumentException if the file does not exist. * @throws DropboxUnlinkedException if you have not set an access token * pair on the session, or if the user has revoked access. * @throws DropboxFileSizeException if the file is bigger than the * maximum allowed by the API. See * {@code DropboxAPI.MAX_UPLOAD_SIZE}. * @throws DropboxServerException if the server responds with an error * code. See the constants in {@link DropboxServerException} for * the meaning of each error code. The most common error codes you * can expect from this call are 404 (path to upload not found), * 507 (user over quota), and 400 (unexpected parent rev). * @throws DropboxIOException if any network-related error occurs. * @throws DropboxException for any other unknown errors. This is also a * superclass of all other Dropbox exceptions, so you may want to * only catch this exception which signals that some kind of error * occurred. */ public Entry putFile(String path, InputStream is, long length, String parentRev, ProgressListener listener) throws DropboxException { UploadRequest request = putFileRequest(path, is, length, parentRev, listener); return request.upload(); } /** * Creates a request to upload a file to Dropbox, which you can then * {@code upload()} or {@code abort()}. The upload will not overwrite any * existing version of the file, unless the latest version has the same rev * as the parentRev given. Pass in null if you're expecting this to create * a new file. * * @param path the full Dropbox path where to put the file, including * directories and filename. * @param is the {@link InputStream} from which to upload. * @param length the amount of bytes to read from the {@link InputStream}. * @param parentRev the rev of the file at which the user started editing * it (obtained from a metadata call), or null if this is a new * upload. If null, or if it does not match the latest rev on the * server, a copy of the file will be created and you'll receive * the new metadata upon executing the request. * @param listener an optional {@link ProgressListener} to receive upload * progress updates, or null. * * @return an {@link UploadRequest}. * * @throws IllegalArgumentException if the file does not exist. * @throws DropboxUnlinkedException if you have not set an access token * pair on the session, or if the user has revoked access. * @throws DropboxFileSizeException if the file is bigger than the * maximum allowed by the API. See * {@code DropboxAPI.MAX_UPLOAD_SIZE}. * @throws DropboxException for any other unknown errors. This is also a * superclass of all other Dropbox exceptions, so you may want to * only catch this exception which signals that some kind of error * occurred. */ public UploadRequest putFileRequest(String path, InputStream is, long length, String parentRev, ProgressListener listener) throws DropboxException { return putFileRequest(path, is, length, false, parentRev, listener); } /** * Uploads a file to Dropbox. The upload will overwrite any existing * version of the file. Use {@code putFileRequest()} if you want to be able * to cancel the upload. If you expect the user to be able * to edit a file remotely and locally, then conflicts may arise and * you won't want to use this call: see {@code putFileRequest} instead. * * @param path the full Dropbox path where to put the file, including * directories and filename. * @param is the {@link InputStream} from which to upload. * @param length the amount of bytes to read from the {@link InputStream}. * @param listener an optional {@link ProgressListener} to receive upload * progress updates, or null. * * @return a metadata {@link Entry} representing the uploaded file. * * @throws IllegalArgumentException if the file does not exist. * @throws DropboxUnlinkedException if you have not set an access token * pair on the session, or if the user has revoked access. * @throws DropboxFileSizeException if the file is bigger than the * maximum allowed by the API. See * {@code DropboxAPI.MAX_UPLOAD_SIZE}. * @throws DropboxServerException if the server responds with an error * code. See the constants in {@link DropboxServerException} for * the meaning of each error code. The most common error codes you * can expect from this call are 404 (path to upload not found), * 507 (user over quota), and 400 (unexpected parent rev). * @throws DropboxIOException if any network-related error occurs. * @throws DropboxException for any other unknown errors. This is also a * superclass of all other Dropbox exceptions, so you may want to * only catch this exception which signals that some kind of error * occurred. */ public Entry putFileOverwrite(String path, InputStream is, long length, ProgressListener listener) throws DropboxException { UploadRequest request = putFileOverwriteRequest(path, is, length, listener); return request.upload(); } /** * Creates a request to upload a file to Dropbox, which you can then * {@code upload()} or {@code abort()}. The upload will overwrite any * existing version of the file. If you expect the user to be able * to edit a file remotely and locally, then conflicts may arise and * you won't want to use this call: see {@code putFileRequest} instead. * * @param path the full Dropbox path where to put the file, including * directories and filename. * @param is the {@link InputStream} from which to upload. * @param length the amount of bytes to read from the {@link InputStream}. * @param listener an optional {@link ProgressListener} to receive upload * progress updates, or null. * * @return an {@link UploadRequest}. * * @throws IllegalArgumentException if the file does not exist locally. * @throws DropboxUnlinkedException if you have not set an access token * pair on the session, or if the user has revoked access. * @throws DropboxFileSizeException if the file is bigger than the * maximum allowed by the API. See * {@code DropboxAPI.MAX_UPLOAD_SIZE}. * @throws DropboxException for any other unknown errors. This is also a * superclass of all other Dropbox exceptions, so you may want to * only catch this exception which signals that some kind of error * occurred. */ public UploadRequest putFileOverwriteRequest(String path, InputStream is, long length, ProgressListener listener) throws DropboxException { return putFileRequest(path, is, length, true, null, listener); } /** * Downloads a thumbnail from Dropbox, copying it to the output stream. * Returns the {@link DropboxFileInfo} for the downloaded thumbnail. * * @param path the Dropbox path to the file for which you want to get a * thumbnail. * @param os the {@link OutputStream} to write the thumbnail to. * @param size the size of the thumbnail to download. * @param format the image format of the thumbnail to download. * @param listener an optional {@link ProgressListener} to receive progress * updates as the thumbnail downloads, or null. * * @return the {@link DropboxFileInfo} for the downloaded thumbnail. * * @throws DropboxUnlinkedException if you have not set an access token * pair on the session, or if the user has revoked access. * @throws DropboxServerException if the server responds with an error * code. See the constants in {@link DropboxServerException} for * the meaning of each error code. The most common error codes you * can expect from this call are 404 (path not found or can't be * thumbnailed), 415 (this type of file can't be thumbnailed), and * 500 (internal error while creating thumbnail). * @throws DropboxPartialFileException if a network error occurs during the * download. * @throws DropboxIOException for some network-related errors. * @throws DropboxException for any other unknown errors. This is also a * superclass of all other Dropbox exceptions, so you may want to * only catch this exception which signals that some kind of error * occurred. */ public DropboxFileInfo getThumbnail(String path, OutputStream os, ThumbSize size, ThumbFormat format, ProgressListener listener) throws DropboxException { DropboxInputStream thumb = getThumbnailStream(path, size, format); thumb.copyStreamToOutput(os, listener); return thumb.getFileInfo(); } /** * Downloads a thumbnail from Dropbox. Returns a {@link DropboxInputStream} * via which the thumbnail can be read from the network. You must close the * stream when you're done with it to release all resources. * * You can also cancel the thumbnail download by closing the returned * {@link DropboxInputStream} at any time. * * @param path the Dropbox path to the file for which you want to get a * thumbnail. * @param size the size of the thumbnail to download. * @param format the image format of the thumbnail to download. * * @return a {@link DropboxInputStream} from which to read the thumbnail. * The contents are retrieved from the network and not stored * locally. * * @throws DropboxUnlinkedException if you have not set an access token * pair on the session, or if the user has revoked access. * @throws DropboxServerException if the server responds with an error * code. See the constants in {@link DropboxServerException} for * the meaning of each error code. The most common error codes you * can expect from this call are 404 (path not found or can't be * thumbnailed), 415 (this type of file can't be thumbnailed), and * 500 (internal error while creating thumbnail) * @throws DropboxIOException if any network-related error occurs. * @throws DropboxException for any other unknown errors. This is also a * superclass of all other Dropbox exceptions, so you may want to * only catch this exception which signals that some kind of error * occurred. */ public DropboxInputStream getThumbnailStream(String path, ThumbSize size, ThumbFormat format) throws DropboxException { assertAuthenticated(); String target = "/thumbnails/" + session.getAccessType() + path; String[] params = {"size", size.toAPISize(), "format", format.toString(), "locale", session.getLocale().toString()}; RequestAndResponse rr = RESTUtility.streamRequest(RequestMethod.GET, session.getContentServer(), target, VERSION, params, session); return new DropboxInputStream(rr.request, rr.response); } /** * Returns the metadata for a file, or for a directory and (optionally) its * immediate children. * * @param path the Dropbox path to the file or directory for which to get * metadata. * @param fileLimit the maximum number of children to return for a * directory. Default is 25,000 if you pass in 0 or less. If there * are too many entries to return, you will get a 406 * {@link DropboxServerException}. Pass in 1 if getting metadata * for a file. * @param hash if you previously got metadata for a directory and have it * stored, pass in the returned hash. If the directory has not * changed since you got the hash, a 304 * {@link DropboxServerException} will be thrown. Pass in null for * files or unknown directories. * @param list if true, returns metadata for a directory's immediate * children, or just the directory entry itself if false. Ignored * for files. * @param rev optionally gets metadata for a file at a prior rev (does not * apply to folders). Use null for the latest metadata. * * @return a metadata {@link Entry}. * * @throws DropboxUnlinkedException if you have not set an access token * pair on the session, or if the user has revoked access. * @throws DropboxServerException if the server responds with an error * code. See the constants in {@link DropboxServerException} for * the meaning of each error code. The most common error codes you * can expect from this call are 304 (contents haven't changed * based on the hash), 404 (path not found or unknown rev for * path), and 406 (too many entries to return). * @throws DropboxIOException if any network-related error occurs. * @throws DropboxException for any other unknown errors. This is also a * superclass of all other Dropbox exceptions, so you may want to * only catch this exception which signals that some kind of error * occurred. */ public Entry metadata(String path, int fileLimit, String hash, boolean list, String rev) throws DropboxException { assertAuthenticated(); if (fileLimit <= 0) { fileLimit = METADATA_DEFAULT_LIMIT; } String[] params = { "file_limit", String.valueOf(fileLimit), "hash", hash, "list", String.valueOf(list), "rev", rev, "locale", session.getLocale().toString() }; String url_path = "/metadata/" + session.getAccessType() + path; @SuppressWarnings("unchecked") Map<String, Object> dirinfo = (Map<String, Object>) RESTUtility.request(RequestMethod.GET, session.getAPIServer(), url_path, VERSION, params, session); return new Entry(dirinfo); } /** * Returns a list of metadata for all revs of the path. * * @param path the Dropbox path to the file for which to get revisions * (directories are not supported). * @param revLimit the maximum number of revisions to return. Default is * 1,000 if you pass in 0 or less, and 1,000 is the most that will * ever be returned. * * @return a list of metadata entries. * * @throws DropboxUnlinkedException if you have not set an access token * pair on the session, or if the user has revoked access. * @throws DropboxServerException if the server responds with an error * code. See the constants in {@link DropboxServerException} for * the meaning of each error code. The most common error code you * can expect from this call is 404 (no revisions found for path). * @throws DropboxIOException if any network-related error occurs. * @throws DropboxException for any other unknown errors. This is also a * superclass of all other Dropbox exceptions, so you may want to * only catch this exception which signals that some kind of error * occurred. */ @SuppressWarnings("unchecked") public List<Entry> revisions(String path, int revLimit) throws DropboxException { assertAuthenticated(); if (revLimit <= 0) { revLimit = REVISION_DEFAULT_LIMIT; } String[] params = { "rev_limit", String.valueOf(revLimit), "locale", session.getLocale().toString() }; String url_path = "/revisions/" + session.getAccessType() + path; JSONArray revs = (JSONArray)RESTUtility.request(RequestMethod.GET, session.getAPIServer(), url_path, VERSION, params, session); List<Entry> entries = new LinkedList<Entry>(); for (Object metadata : revs) { entries.add(new Entry((Map<String, Object>)metadata)); } return entries; } /** * Searches a directory for entries matching the query. * * @param path the Dropbox directory to search in. * @param query the query to search for (minimum 3 characters). * @param fileLimit the maximum number of file entries to return. Default * is 10,000 if you pass in 0 or less, and 1,000 is the most that * will ever be returned. * @param includeDeleted whether to include deleted files in search * results. * * @return a list of metadata entries of matching files. * * @throws DropboxUnlinkedException if you have not set an access token * pair on the session, or if the user has revoked access. * @throws DropboxServerException if the server responds with an error * code. See the constants in {@link DropboxServerException} for * the meaning of each error code. * @throws DropboxIOException if any network-related error occurs. * @throws DropboxException for any other unknown errors. This is also a * superclass of all other Dropbox exceptions, so you may want to * only catch this exception which signals that some kind of error * occurred. */ public List<Entry> search(String path, String query, int fileLimit, boolean includeDeleted) throws DropboxException { assertAuthenticated(); if (fileLimit <= 0) { fileLimit = SEARCH_DEFAULT_LIMIT; } String target = "/search/" + session.getAccessType() + path; String[] params = { "query", query, "file_limit", String.valueOf(fileLimit), "include_deleted", String.valueOf(includeDeleted), "locale", session.getLocale().toString() }; Object response = RESTUtility.request(RequestMethod.GET, session.getAPIServer(), target, VERSION, params, session); ArrayList<Entry> ret = new ArrayList<Entry>(); if (response instanceof JSONArray) { JSONArray jresp = (JSONArray)response; for (Object next: jresp) { if (next instanceof Map) { @SuppressWarnings("unchecked") Entry ent = new Entry((Map<String, Object>)next); ret.add(ent); } } } return ret; } /** * Moves a file or folder (and all of the folder's contents) from one path * to another. * * @param fromPath the Dropbox path to move from. * @param toPath the full Dropbox path to move to (not just a directory). * * @return a metadata {@link Entry}. * * @throws DropboxUnlinkedException if you have not set an access token * pair on the session, or if the user has revoked access. * @throws DropboxServerException if the server responds with an error * code. See the constants in {@link DropboxServerException} for * the meaning of each error code. The most common error codes you * can expect from this call are 403 (operation is forbidden), 404 * (path not found), and 507 (user over quota). * @throws DropboxIOException if any network-related error occurs. * @throws DropboxException for any other unknown errors. This is also a * superclass of all other Dropbox exceptions, so you may want to * only catch this exception which signals that some kind of error * occurred. */ public Entry move(String fromPath, String toPath) throws DropboxException { assertAuthenticated(); String[] params = {"root", session.getAccessType().toString(), "from_path", fromPath, "to_path", toPath, "locale", session.getLocale().toString()}; @SuppressWarnings("unchecked") Map<String, Object> resp = (Map<String, Object>)RESTUtility.request( RequestMethod.POST, session.getAPIServer(), "/fileops/move", VERSION, params, session); return new Entry(resp); } /** * Copies a file or folder (and all of the folder's contents) from one path * to another. * * @param fromPath the Dropbox path to copy from. * @param toPath the full Dropbox path to copy to (not just a directory). * * @return a metadata {@link Entry}. * * @throws DropboxUnlinkedException if you have not set an access token * pair on the session, or if the user has revoked access. * @throws DropboxServerException if the server responds with an error * code. See the constants in {@link DropboxServerException} for * the meaning of each error code. The most common error codes you * can expect from this call are 403 (operation is forbidden), 404 * (path not found), and 507 (user over quota). * @throws DropboxIOException if any network-related error occurs. * @throws DropboxException for any other unknown errors. This is also a * superclass of all other Dropbox exceptions, so you may want to * only catch this exception which signals that some kind of error * occurred. */ public Entry copy(String fromPath, String toPath) throws DropboxException { assertAuthenticated(); String[] params = {"root", session.getAccessType().toString(), "from_path", fromPath, "to_path", toPath, "locale", session.getLocale().toString()}; @SuppressWarnings("unchecked") Map<String, Object> resp = (Map<String, Object>)RESTUtility.request( RequestMethod.POST, session.getAPIServer(), "/fileops/copy", VERSION, params, session); return new Entry(resp); } /** * Creates a new Dropbox folder. * * @param path the Dropbox path to the new folder. * * @return a metadata {@link Entry} for the new folder. * * @throws DropboxUnlinkedException if you have not set an access token * pair on the session, or if the user has revoked access. * @throws DropboxServerException if the server responds with an error * code. See the constants in {@link DropboxServerException} for * the meaning of each error code. The most common error codes you * can expect from this call are 403 (something already exists at * that path), 404 (path not found), and 507 (user over quota). * @throws DropboxIOException if any network-related error occurs. * @throws DropboxException for any other unknown errors. This is also a * superclass of all other Dropbox exceptions, so you may want to * only catch this exception which signals that some kind of error * occurred. */ public Entry createFolder(String path) throws DropboxException { assertAuthenticated(); String[] params = {"root", session.getAccessType().toString(), "path", path, "locale", session.getLocale().toString()}; @SuppressWarnings("unchecked") Map<String, Object> resp = (Map<String, Object>) RESTUtility.request( RequestMethod.POST, session.getAPIServer(), "/fileops/create_folder", VERSION, params, session); return new Entry(resp); } /** * Deletes a file or folder (and all of the folder's contents). After * deletion, metadata calls may still return this file or folder for some * time, but the metadata {@link Entry}'s {@code isDeleted} attribute will * be set to {@code true}. * * @param path the Dropbox path to delete. * * @throws DropboxUnlinkedException if you have not set an access token * pair on the session, or if the user has revoked access. * @throws DropboxServerException if the server responds with an error * code. See the constants in {@link DropboxServerException} for * the meaning of each error code. The most common error code you * can expect from this call is 404 (path not found). * @throws DropboxIOException if any network-related error occurs. * @throws DropboxException for any other unknown errors. This is also a * superclass of all other Dropbox exceptions, so you may want to * only catch this exception which signals that some kind of error * occurred. */ public void delete(String path) throws DropboxException { assertAuthenticated(); String[] params = {"root", session.getAccessType().toString(), "path", path, "locale", session.getLocale().toString()}; RESTUtility.request(RequestMethod.POST, session.getAPIServer(), "/fileops/delete", VERSION, params, session); } /** * Restores a file to a previous rev. * * @param path the Dropbox path to the file to restore. * @param rev the rev to restore to (obtained from a metadata or revisions * call). * * @return a metadata {@link Entry} for the newly restored file. * * @throws DropboxUnlinkedException if you have not set an access token * pair on the session, or if the user has revoked access. * @throws DropboxServerException if the server responds with an error * code. See the constants in {@link DropboxServerException} for * the meaning of each error code. The most common error code you * can expect from this call is 404 (path not found or unknown * revision). * @throws DropboxIOException if any network-related error occurs. * @throws DropboxException for any other unknown errors. This is also a * superclass of all other Dropbox exceptions, so you may want to * only catch this exception which signals that some kind of error * occurred. */ public Entry restore(String path, String rev) throws DropboxException { assertAuthenticated(); String[] params = { "rev", rev, "locale", session.getLocale().toString() }; String target = "/restore/" + session.getAccessType() + path; @SuppressWarnings("unchecked") Map<String, Object> metadata = (Map<String, Object>)RESTUtility.request(RequestMethod.GET, session.getAPIServer(), target, VERSION, params, session); return new Entry(metadata); } /** * Returns a {@link DropboxLink} for a stream of the given file path (for * streaming media files). * * @param path the Dropbox path of the file for which to get a streaming * link. * @param ssl whether the streaming URL is https or http. Some Android * and other platforms won't play https streams, so false converts * the link to an http link before returning it. * * @return a {@link DropboxLink} for streaming the file. * * @throws DropboxUnlinkedException if you have not set an access token * pair on the session, or if the user has revoked access. * @throws DropboxServerException if the server responds with an error * code. See the constants in {@link DropboxServerException} for * the meaning of each error code. The most common error code you * can expect from this call is 404 (path not found). * @throws DropboxIOException if any network-related error occurs. * @throws DropboxException for any other unknown errors. This is also a * superclass of all other Dropbox exceptions, so you may want to * only catch this exception which signals that some kind of error * occurred. */ public DropboxLink media(String path, boolean ssl) throws DropboxException { assertAuthenticated(); String target = "/media/" + session.getAccessType() + path; @SuppressWarnings("unchecked") Map<String, Object> map = (Map<String, Object>)RESTUtility.request(RequestMethod.GET, session.getAPIServer(), target, VERSION, new String[] {"locale", session.getLocale().toString()}, session); return new DropboxLink(map, ssl); } /** * Generates a {@link DropboxLink} for sharing the specified directory or * file. * * @param path the Dropbox path to share, either a directory or file. * * @return a {@link DropboxLink} for the path. * * @throws DropboxUnlinkedException if you have not set an access token * pair on the session, or if the user has revoked access. * @throws DropboxServerException if the server responds with an error * code. See the constants in {@link DropboxServerException} for * the meaning of each error code. The most common error code you * can expect from this call is 404 (path not found). * @throws DropboxIOException if any network-related error occurs. * @throws DropboxException for any other unknown errors. This is also a * superclass of all other Dropbox exceptions, so you may want to * only catch this exception which signals that some kind of error * occurred. */ public DropboxLink share(String path) throws DropboxException { assertAuthenticated(); String target = "/shares/" + session.getAccessType() + path; @SuppressWarnings("unchecked") Map<String, Object> map = (Map<String, Object>)RESTUtility.request(RequestMethod.GET, session.getAPIServer(), target, VERSION, new String[] {"locale", session.getLocale().toString()}, session); String url = (String)map.get("url"); Date expires = RESTUtility.parseDate((String)map.get("expires")); if (url == null || expires == null) { throw new DropboxParseException("Could not parse share response."); } return new DropboxLink(map); } /** * Helper function to read boolean JSON return values * * @param map * the one to read from * @param name * the parameter name to read * @return the value, with false as a default if no parameter set */ protected static boolean getFromMapAsBoolean(Map<String, Object> map, String name) { Object val = map.get(name); if (val != null && val instanceof Boolean) { return ((Boolean) val).booleanValue(); } else { return false; } } /** * Creates a request to upload an {@link InputStream} to a Dropbox file. * You can then {@code upload()} or {@code abort()} this request. This is * the advanced version, which you should only use if you really need the * flexibility of uploading using an {@link InputStream}. * * @param path the full Dropbox path where to put the file, including * directories and filename. * @param is the {@link InputStream} from which to upload. * @param length the amount of bytes to read from the {@link InputStream}. * @param overwrite whether to overwrite the file if it already exists. If * true, any existing file will always be overwritten. If false, * files will be overwritten only if the {@code parentRev} matches * the current rev on the server or otherwise a conflicted copy of * the file will be created and you will get the new file's * metadata {@link Entry}. * @param parentRev the rev of the file at which the user started editing * it (obtained from a metadata call), or null if this is a new * upload. If null, or if it does not match the latest rev on the * server, a copy of the file will be created and you'll receive * the new metadata upon executing the request. * @param listener an optional {@link ProgressListener} to receive upload * progress updates, or null. * * @return an {@link UploadRequest}. * * @throws IllegalArgumentException if {@code newFilename} is null or * empty. * @throws DropboxUnlinkedException if you have not set an access token * pair on the session, or if the user has revoked access. * @throws DropboxFileSizeException if the file is bigger than the * maximum allowed by the API. See * {@code DropboxAPI.MAX_UPLOAD_SIZE}. * @throws DropboxException for any other unknown errors. This is also a * superclass of all other Dropbox exceptions, so you may want to * only catch this exception which signals that some kind of error * occurred. */ private UploadRequest putFileRequest(String path, InputStream is, long length, boolean overwrite, String parentRev, ProgressListener listener) throws DropboxException { if (path == null || path.equals("")) { throw new IllegalArgumentException("path is null or empty."); } assertAuthenticated(); if (!path.startsWith("/")) { path = "/" + path; } String target = "/files_put/" + session.getAccessType() + path; if (parentRev == null) { parentRev = ""; } String[] params = new String[] { "overwrite", String.valueOf(overwrite), "parent_rev", parentRev, "locale", session.getLocale().toString() }; String url = RESTUtility.buildURL(session.getContentServer(), VERSION, target, params); HttpPut req = new HttpPut(url); session.sign(req); InputStreamEntity isEntity = new InputStreamEntity(is, length); isEntity.setContentEncoding("application/octet-stream"); isEntity.setChunked(false); HttpEntity entity = isEntity; if (listener != null) { entity = new ProgressHttpEntity(entity, listener); } req.setEntity(entity); return new BasicUploadRequest(req, session); } /** * A way of letting you keep up with changes to files and folders in a user's * Dropbox. You can periodically call this function to get a list of "delta * entries", which are instructions on how to update your local state to match * the server's state. * * @param cursor * On the first call, you should pass in <code>null</code>. On subsequent * calls, pass in the {@link DeltaPage#cursor cursor} returned by the previous * call. * * @return * A single {@link DeltaPage DeltaPage} of results. The {@link DeltaPage#hasMore hasMore} * field will tell you whether the server has more pages of results to return. * If the server doesn't have more results, you can wait a bit (say, * 5 or 10 minutes) and poll again. * * @throws DropboxUnlinkedException if you have not set an access token * pair on the session, or if the user has revoked access. * @throws DropboxException for any other unknown errors. This is also a * superclass of all other Dropbox exceptions, so you may want to * only catch this exception which signals that some kind of error * occurred. */ public DeltaPage<Entry> delta(String cursor) throws DropboxException { String[] params = new String[] { "cursor", cursor, "locale", session.getLocale().toString(), }; Object json = RESTUtility.request(RequestMethod.POST, session.getAPIServer(), "/delta", VERSION, params, session); try { return DeltaPage.extractFromJson(new JsonThing(json), Entry.JsonExtractor); } catch (JsonExtractionException ex) { throw new DropboxParseException("Error parsing /delta results: " + ex.getMessage()); } } /** * A page of {@link DeltaEntry DeltaEntry}s (returned by {@link #delta delta}). */ public static final class DeltaPage<MD> { /** * If <code>true</code>, then you should reset your local state to be an empty * folder before processing the list of delta entries. This is only <code>true</code> * in rare situations. */ public final boolean reset; /** * A string that is used to keep track of your current state. On the next call to * {@link #delta delta}, pass in this value to pick up where you left off. */ public final String cursor; /** * Apply these entries to your local state to catch up with the Dropbox server's state. */ public final List<DeltaEntry<MD>> entries; /** * If <code>true</code>, then there are more entries available; you can call {@link * #delta delta} again immediately to retrieve those entries. If <code>false</code>, * then wait at least 5 minutes (preferably longer) before checking again. */ public final boolean hasMore; public DeltaPage(boolean reset, List<DeltaEntry<MD>> entries, String cursor, boolean hasMore) { this.reset = reset; this.entries = entries; this.cursor = cursor; this.hasMore = hasMore; } public static <MD> DeltaPage<MD> extractFromJson(JsonThing j, JsonExtractor<MD> entryExtractor) throws JsonExtractionException { JsonMap m = j.expectMap(); boolean reset = m.get("reset").expectBoolean(); String cursor = m.get("cursor").expectString(); boolean hasMore = m.get("has_more").expectBoolean(); List<DeltaEntry<MD>> entries = m.get("entries").expectList().extract(new DeltaEntry.JsonExtractor<MD>(entryExtractor)); return new DeltaPage<MD>(reset, entries, cursor, hasMore); } } /** * A single entry in a {@link DeltaPage DeltaPage}. */ public static final class DeltaEntry<MD> { /** * The lower-cased path of the entry. Dropbox compares file paths in a * case-insensitive manner. For example, an entry for <code>"/readme.txt"</code> * should overwrite the entry for <code>"/ReadMe.TXT"</code>. * * <p> * To get the original case-preserved path, look in the {@link #metadata metadata} field. * </p> */ public final String lcPath; /** * If this is <code>null</code>, it means that this path doesn't exist on * on Dropbox's copy of the file system. To update your local state to * match, delete whatever is at that path, including any children. * If your local state doesn't have anything at this path, ignore this entry. * * <p> * If this is not <code>null</code>, it means that Dropbox has a file/folder * at this path with the given metadata. To update your local state to match, * add the entry to your local state as well. * </p> * <ul> * <li> * If the path refers to parent folders that don't exist yet in your local * state, create those parent folders in your local state. * </li> * <li> * If the metadata is for a file, replace whatever your local state has at * that path with the new entry. * </li> * <li> * If the metadata is for a folder, check what your local state has at the * path. If it's a file, replace it with the new entry. If it's a folder, * apply the new metadata to the folder, but do not modify the folder's * children. * </li> * </ul> */ public final MD metadata; public DeltaEntry(String lcPath, MD metadata) { this.lcPath = lcPath; this.metadata = metadata; } public static final class JsonExtractor<MD> extends com.dropbox.client2.jsonextract.JsonExtractor<DeltaEntry<MD>> { public final com.dropbox.client2.jsonextract.JsonExtractor<MD> mdExtractor; public JsonExtractor(com.dropbox.client2.jsonextract.JsonExtractor<MD> mdExtractor) { this.mdExtractor = mdExtractor; } public DeltaEntry<MD> extract(JsonThing j) throws JsonExtractionException { return extract(j, this.mdExtractor); } public static <MD> DeltaEntry<MD> extract(JsonThing j, com.dropbox.client2.jsonextract.JsonExtractor<MD> mdExtractor) throws JsonExtractionException { JsonList l = j.expectList(); String path = l.get(0).expectString(); MD metadata = l.get(1).optionalExtract(mdExtractor); return new DeltaEntry<MD>(path, metadata); } } } /** * Creates a reference to a path that can be used with {@link #addFromCopyRef * addFromCopyRef()} to copy the contents of the file at that path to a * different Dropbox account. This is more efficient than copying the content * * @param sourcePath * The full path to the file that you want a * * @return * A string representation of the file pointer. * * @throws DropboxUnlinkedException if you have not set an access token * pair on the session, or if the user has revoked access. * @throws DropboxException for any other unknown errors. This is also a * superclass of all other Dropbox exceptions, so you may want to * only catch this exception which signals that some kind of error * occurred. */ public CreatedCopyRef createCopyRef(String sourcePath) throws DropboxException { assertAuthenticated(); if (!sourcePath.startsWith("/")) { throw new IllegalArgumentException("'sourcePath' must start with \"/\": " + sourcePath); } String[] params = { "locale", session.getLocale().toString() }; String url_path = "/copy_ref/" + session.getAccessType() + sourcePath; @SuppressWarnings("unchecked") Object result = RESTUtility.request(RequestMethod.GET, session.getAPIServer(), url_path, VERSION, params, session); try { return CreatedCopyRef.extractFromJson(new JsonThing(result)); } catch (JsonExtractionException ex) { throw new DropboxParseException("Error parsing /copy_ref results: " + ex.getMessage()); } } public static final class CreatedCopyRef { public final String copyRef; public final String expiration; public CreatedCopyRef(String copyRef, String expiration) { this.copyRef = copyRef; this.expiration = expiration; } public static CreatedCopyRef extractFromJson(JsonThing j) throws JsonExtractionException { JsonMap m = j.expectMap(); String string = m.get("copy_ref").expectString(); String expiration = m.get("expires").expectString(); return new CreatedCopyRef(string, expiration); } } /** * Creates a file in the Dropbox that the client is currently connected to, using * the contents from a {@link com.dropbox.client2.DropboxAPI.CreatedCopyRef CopyRef} created with * {@link #createCopyRef createCopyRef()}. The {@link CreatedCopyRef CreatedCopyRef} * can be for a file in a different Dropbox account. * * @param sourceCopyRef * The copy-ref to use as the source of the file data (comes from * {@link CreatedCopyRef#copyRef CreatedCopyRef.copyRef}, which is created * through {@link #createCopyRef createCopyRef()}). * @param targetPath * The path that you want to create the file at. * * @return * The {@link Entry} for the new file. * * @throws DropboxUnlinkedException if you have not set an access token * pair on the session, or if the user has revoked access. * @throws DropboxException for any other unknown errors. This is also a * superclass of all other Dropbox exceptions, so you may want to * only catch this exception which signals that some kind of error * occurred. */ public Entry addFromCopyRef(String sourceCopyRef, String targetPath) throws DropboxException { assertAuthenticated(); if (!targetPath.startsWith("/")) { throw new IllegalArgumentException("'targetPath' doesn't start with \"/\": " + targetPath); } String[] params = { "locale", session.getLocale().toString(), "root", session.getAccessType().toString(), "from_copy_ref", sourceCopyRef, "to_path", targetPath, }; String url_path = "/fileops/copy"; @SuppressWarnings("unchecked") Map<String, Object> dirinfo = (Map<String, Object>) RESTUtility.request(RequestMethod.GET, session.getAPIServer(), url_path, VERSION, params, session); return new Entry(dirinfo); } /** * Throws a {@link DropboxUnlinkedException} if the session in this * instance is not linked. */ protected void assertAuthenticated() throws DropboxUnlinkedException { if (!session.isLinked()) { throw new DropboxUnlinkedException(); } } /** * Helper function to read long JSON return values * * @param map * the one to read from * @param name * the parameter name to read * @return the value, with 0 as a default if no parameter set */ protected static long getFromMapAsLong(Map<String, Object> map, String name) { Object val = map.get(name); long ret = 0; if (val != null) { if (val instanceof Number) { ret = ((Number) val).longValue(); } else if (val instanceof String) { // To parse cases where JSON can't represent a Long, so // it's stored as a string ret = Long.parseLong((String)val, 16); } } return ret; } }