package com.seafile.seadroid2; import android.content.Context; import android.content.pm.PackageInfo; import android.content.pm.PackageManager.NameNotFoundException; import android.os.Build; import android.provider.Settings.Secure; import android.text.TextUtils; import android.util.Log; import android.util.Pair; import com.github.kevinsawicki.http.HttpRequest; import com.github.kevinsawicki.http.HttpRequest.HttpRequestException; import com.google.common.collect.Maps; import com.seafile.seadroid2.account.Account; import com.seafile.seadroid2.data.Block; import com.seafile.seadroid2.data.DataManager; import com.seafile.seadroid2.data.FileBlocks; import com.seafile.seadroid2.data.ProgressMonitor; import com.seafile.seadroid2.ssl.SSLTrustManager; import com.seafile.seadroid2.util.Utils; import org.json.JSONException; import org.json.JSONObject; import java.io.File; import java.io.FileInputStream; import java.io.FileOutputStream; import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; import java.io.UnsupportedEncodingException; import java.net.HttpURLConnection; import java.net.URLEncoder; import java.util.List; import java.util.Map; import javax.net.ssl.HttpsURLConnection; import javax.net.ssl.SSLHandshakeException; /** * SeafConnection encapsulates Seafile Web API * @author plt */ public class SeafConnection { public static final int HTTP_STATUS_REPO_PASSWORD_REQUIRED = 440; private static final String DEBUG_TAG = "SeafConnection"; private static final int CONNECTION_TIMEOUT = 15000; private static final int READ_TIMEOUT = 30000; private Account account; public SeafConnection(Account act) { account = act; } public Account getAccount() { return account; } private HttpRequest prepareApiGetRequest(String apiPath, Map<String, ?> params) throws IOException { HttpRequest req = HttpRequest.get(account.server + apiPath, params, false); setRequestCommon(req); return req; } private HttpRequest prepareApiPutRequest(String apiPath, Map<String, ?> params) throws IOException { HttpRequest req = HttpRequest.put(account.server + apiPath, params, false); setRequestCommon(req); return req; } private void setRequestCommon(HttpRequest req) { req.readTimeout(READ_TIMEOUT) .connectTimeout(CONNECTION_TIMEOUT) .followRedirects(true) .header("Authorization", "Token " + account.token); prepareHttpsCheck(req); } private HttpRequest prepareHttpsCheck(HttpRequest req) { HttpURLConnection conn = req.getConnection(); if (conn instanceof HttpsURLConnection) { // Tell HttpRequest to trust all hosts, and then the user will get a dialog // where he needs to confirm the SSL certificate for the account, // and the accepted certificate will be stored, so he is not prompted to accept later on. // This is handled by SSLTrustManager and CertsManager req.trustAllHosts(); HttpsURLConnection sconn = (HttpsURLConnection) conn; sconn.setSSLSocketFactory(SSLTrustManager.instance().getSSLSocketFactory(account)); } return req; } private HttpRequest prepareApiGetRequest(String apiPath) throws IOException { return prepareApiGetRequest(apiPath, null); } private HttpRequest prepareApiFileGetRequest(String url) throws HttpRequestException { HttpRequest req = HttpRequest.get(url).connectTimeout(CONNECTION_TIMEOUT).followRedirects(true); return prepareHttpsCheck(req); } /** Prepare a post request. * @param apiPath The path of the http request * @param withToken * @param params The query param to be appended to the request url * @throws IOException */ private HttpRequest prepareApiPostRequest(String apiPath, boolean withToken, Map<String, ?> params) throws HttpRequestException { return prepareApiPostRequest(apiPath, withToken, params, false); } /** Prepare a post request. * @param apiPath The path of the http request * @param withToken * @param params The query param to be appended to the request url * @param encode true to encode the full URL * @throws IOException */ private HttpRequest prepareApiPostRequest(String apiPath, boolean withToken, Map<String, ?> params, boolean encode) throws HttpRequestException { HttpRequest req = HttpRequest.post(account.server + apiPath, params, encode) .followRedirects(true) .connectTimeout(CONNECTION_TIMEOUT); if (withToken) { req.header("Authorization", "Token " + account.token); } return prepareHttpsCheck(req); } private HttpRequest prepareApiDeleteRequest(String apiPath, Map<String, ?> params) throws HttpRequestException { HttpRequest req = HttpRequest.delete(account.server + apiPath, params, false) .followRedirects(true) .connectTimeout(CONNECTION_TIMEOUT); req.header("Authorization", "Token " + account.token); return prepareHttpsCheck(req); } /** * Login into the server * @return true if login success, false otherwise * @throws SeafException */ private boolean realLogin(String passwd, String authToken) throws SeafException { boolean withAuthToken = false; HttpRequest req = null; try { req = prepareApiPostRequest("api2/auth-token/", false, null); // Log.d(DEBUG_TAG, "Login to " + account.server + "api2/auth-token/"); if (!TextUtils.isEmpty(authToken)) { req.header("X-Seafile-OTP", authToken); withAuthToken = true; // Log.d(DEBUG_TAG, "authToken " + authToken); } req.form("username", account.email); req.form("password", passwd); String appVersion = ""; Context context = SeadroidApplication.getAppContext(); try { PackageInfo pInfo = context.getPackageManager(). getPackageInfo(context.getPackageName(), 0); appVersion = pInfo.versionName; } catch (NameNotFoundException e) { // ignore } String deviceId = Secure.getString(context.getContentResolver(), Secure.ANDROID_ID); req.form("platform", "android"); req.form("device_id", deviceId); req.form("device_name", Build.MODEL); req.form("client_version", appVersion); req.form("platform_version", Build.VERSION.RELEASE); checkRequestResponseStatus(req, HttpURLConnection.HTTP_OK, withAuthToken); String contentAsString = new String(req.bytes(), "UTF-8"); JSONObject obj = Utils.parseJsonObject(contentAsString); if (obj == null) return false; account.token = obj.getString("token"); return true; } catch (SeafException e) { throw e; } catch (HttpRequestException e) { throw getSeafExceptionFromHttpRequestException(e); } catch (IOException e) { e.printStackTrace(); throw SeafException.networkException; } catch (JSONException e) { throw SeafException.illFormatException; } } /** * <p> * get Account info, which consists of three fields, usage, total and email. * </p> * use GET to send HTTP request. * * @return * @throws SeafException */ public String getAccountInfo() throws SeafException { String result; try { HttpRequest req = prepareApiGetRequest("api2/account/info/"); checkRequestResponseStatus(req, HttpURLConnection.HTTP_OK); result = new String(req.bytes(), "UTF-8"); } catch (SeafException e) { throw e; } catch (HttpRequestException e) { throw getSeafExceptionFromHttpRequestException(e); } catch (IOException e) { throw SeafException.networkException; } return result; } public String getServerInfo() throws SeafException { String result; try { HttpRequest req = prepareApiGetRequest("api2/server-info/"); checkRequestResponseStatus(req, HttpURLConnection.HTTP_OK); result = new String(req.bytes(), "UTF-8"); } catch (SeafException e) { throw e; } catch (HttpRequestException e) { throw getSeafExceptionFromHttpRequestException(e); } catch (IOException e) { throw SeafException.networkException; } return result; } public boolean doLogin(String passwd, String authToken) throws SeafException { try { return realLogin(passwd, authToken); } catch (Exception e) { // do again return realLogin(passwd, authToken); } } public String getRepos() throws SeafException { HttpRequest req = null; try { req = prepareApiGetRequest("api2/repos/"); checkRequestResponseStatus(req, HttpURLConnection.HTTP_OK); String result = new String(req.bytes(), "UTF-8"); return result; } catch (SeafException e) { throw e; } catch (HttpRequestException e) { throw getSeafExceptionFromHttpRequestException(e); } catch (IOException e) { throw SeafException.networkException; } } public String getEvents(int start) throws SeafException { try { String apiPath = String.format("api2/events/"); Map<String, Object> params = Maps.newHashMap(); params.put("start", start); HttpRequest req = prepareApiGetRequest(apiPath, params); checkRequestResponseStatus(req, HttpURLConnection.HTTP_OK); return new String(req.bytes(), "UTF-8"); } catch (SeafException e) { throw e; } catch (HttpRequestException e) { throw getSeafExceptionFromHttpRequestException(e); } catch (IOException e) { throw SeafException.networkException; } } public String getHistoryChanges(String repoID, String commitId) throws SeafException { try { String apiPath = String.format("api2/repo_history_changes/%s/", repoID); Map<String, Object> params = Maps.newHashMap(); params.put("commit_id", commitId); HttpRequest req = prepareApiGetRequest(apiPath, params); checkRequestResponseStatus(req, HttpURLConnection.HTTP_OK); String result = new String(req.bytes(), "UTF-8"); return result; } catch (SeafException e) { throw e; } catch (HttpRequestException e) { throw getSeafExceptionFromHttpRequestException(e); } catch (IOException e) { throw SeafException.networkException; } } public String getStarredFiles() throws SeafException { try { HttpRequest req = prepareApiGetRequest("api2/starredfiles/"); checkRequestResponseStatus(req, HttpURLConnection.HTTP_OK); return new String(req.bytes(), "UTF-8"); } catch (SeafException e) { throw e; } catch (HttpRequestException e) { throw getSeafExceptionFromHttpRequestException(e); } catch (IOException e) { throw SeafException.networkException; } } public String getAvatar(String email, int size) throws SeafException { try { String apiPath = String.format("api2/avatars/user/%s/resized/%d", email, size); HttpRequest req = prepareApiGetRequest(apiPath); checkRequestResponseStatus(req, HttpURLConnection.HTTP_OK); String result = new String(req.bytes(), "UTF-8"); return result; } catch (SeafException e) { throw e; } catch (HttpRequestException e) { throw getSeafExceptionFromHttpRequestException(e); } catch (IOException e) { throw SeafException.networkException; } } public String searchLibraries(String query, int page) throws SeafException { try { Map<String, Object> params = Maps.newHashMap(); params.put("q", encodeUriComponent(query)); if (page > 0) params.put("per_page", page); HttpRequest req = prepareApiGetRequest("api2/search/", params); checkRequestResponseStatus(req, HttpURLConnection.HTTP_OK); String result = new String(req.bytes(), "UTF-8"); return result; } catch (SeafException e) { throw e; } catch (HttpRequestException e) { throw getSeafExceptionFromHttpRequestException(e); } catch (IOException e) { throw SeafException.networkException; } } private static String encodeUriComponent(String src) throws UnsupportedEncodingException { return URLEncoder.encode(src, "UTF-8"); } /** * Get the contents of a directory. * @param repoID * @param path * @param cachedDirID The local cached dirID. * @return A non-null Pair of (dirID, content). If the local cache is up to date, the "content" is null. * @throws SeafException */ public Pair<String, String> getDirents(String repoID, String path, String cachedDirID) throws SeafException { try { String apiPath = String.format("api2/repos/%s/dir/", repoID); Map<String, Object> params = Maps.newHashMap(); params.put("p", encodeUriComponent(path)); if (cachedDirID != null) { params.put("oid", cachedDirID); } HttpRequest req = prepareApiGetRequest(apiPath, params); checkRequestResponseStatus(req, HttpURLConnection.HTTP_OK); String dirID = req.header("oid"); String content; if (dirID == null) { throw SeafException.unknownException; } if (dirID.equals(cachedDirID)) { // local cache is valid // Log.d(DEBUG_TAG, String.format("dir %s is cached", path)); content = null; } else { /*Log.d(DEBUG_TAG, String.format("dir %s will be downloaded from server, latest %s, local cache %s", path, dirID, cachedDirID != null ? cachedDirID : "null"));*/ byte[] rawBytes = req.bytes(); if (rawBytes == null) { throw SeafException.unknownException; } content = new String(rawBytes, "UTF-8"); } return new Pair<String, String>(dirID, content); } catch (SeafException e) { throw e; } catch (UnsupportedEncodingException e) { throw SeafException.encodingException; } catch (HttpRequestException e) { throw getSeafExceptionFromHttpRequestException(e); } catch (IOException e) { throw SeafException.networkException; } } private Pair<String, String> getDownloadLink(String repoID, String path) throws SeafException { try { String apiPath = String.format("api2/repos/%s/file/", repoID); Map<String, Object> params = Maps.newHashMap(); params.put("p", encodeUriComponent(path)); params.put("op", "download"); HttpRequest req = prepareApiGetRequest(apiPath, params); checkRequestResponseStatus(req, HttpURLConnection.HTTP_OK); String result = new String(req.bytes(), "UTF-8"); String fileID = req.header("oid"); // should return "\"http://gonggeng.org:8082/...\"" or "\"https://gonggeng.org:8082/...\" if (result.startsWith("\"http") && fileID != null) { String url = result.substring(1, result.length() - 1); return new Pair<String, String>(url, fileID); } else { throw SeafException.illFormatException; } } catch (SeafException e) { throw e; } catch (UnsupportedEncodingException e) { throw SeafException.encodingException; } catch (IOException e) { throw SeafException.networkException; } catch (HttpRequestException e) { throw getSeafExceptionFromHttpRequestException(e); } } public String getBlockDownloadList(String repoID, String path) throws SeafException, IOException { try { String apiPath = String.format("api2/repos/%s/file/", repoID); Map<String, Object> params = Maps.newHashMap(); params.put("p", encodeUriComponent(path)); params.put("op", "downloadblks"); HttpRequest req = prepareApiGetRequest(apiPath, params); checkRequestResponseStatus(req, HttpURLConnection.HTTP_OK); String result = new String(req.bytes(), "UTF-8"); return result; } catch (SeafException | IOException e) { throw e; } catch (HttpRequestException e) { throw getSeafExceptionFromHttpRequestException(e); } } /** * get file server link for downloading a block * * @param repoID * @param fileId * @param blockId * @return * @throws SeafException * @throws IOException */ private String getBlockDownloadLink(String repoID, String fileId, String blockId) throws SeafException, IOException { try { String apiPath = String.format("api2/repos/%s/files/%s/blks/%s/download-link/", repoID, fileId, blockId); HttpRequest req = prepareApiGetRequest(apiPath, null); checkRequestResponseStatus(req, HttpURLConnection.HTTP_OK); return new String(req.bytes(), "UTF-8"); } catch (SeafException | IOException e) { throw e; } catch (HttpRequestException e) { throw getSeafExceptionFromHttpRequestException(e); } } /** * Get the latest version of the file from server * @param repoID * @param fileBlocks * @param blockId * @param localPath * @param monitor * @return A two tuple of (fileID, file). If the local cached version is up to date, the returned file is null. */ public Pair<String, File> getBlock(String repoID, FileBlocks fileBlocks, String blockId, String localPath, long fileSize, ProgressMonitor monitor) throws SeafException, IOException, JSONException { String dlink = getBlockDownloadLink(repoID, fileBlocks.fileID, blockId).replaceAll("\"", ""); File block = getBlockFromLink(dlink, fileBlocks, blockId, localPath, fileSize, monitor); if (block != null) { return new Pair<>(blockId, block); } else { throw SeafException.unknownException; } } public String uploadByBlocks(String repoID, String dir, String filePath, List<Block> blocks, boolean update, ProgressMonitor monitor) throws IOException, SeafException { try { String url = getUploadLink(repoID, update, true); Log.d(DEBUG_TAG, "UploadLink " + url); return uploadBlocksCommon(url, repoID, dir, filePath, blocks, monitor, update); } catch (SeafException e) { // do again String url = getUploadLink(repoID, update, true); Log.d(DEBUG_TAG, "do again UploadLink " + url); return uploadBlocksCommon(url, repoID, dir, filePath, blocks, monitor, update); } } private File getFileFromLink(String dlink, String path, String localPath, String oid, ProgressMonitor monitor) throws SeafException { if (dlink == null) return null; File file = new File(localPath); try { int i = dlink.lastIndexOf('/'); String quoted = dlink.substring(0, i) + "/" + URLEncoder.encode(dlink.substring(i + 1), "UTF-8"); HttpRequest req = prepareApiFileGetRequest(quoted); checkRequestResponseStatus(req, HttpURLConnection.HTTP_OK); if (monitor != null) { Long size = -1L; if (req.contentLength() > 0) { size = Long.valueOf(req.contentLength()); } else { // The req.contentLength() returns an int, which has a max value of // 2GB. So if a file size exceeds 2GB, request.contentLength() would // return -1. In such case, we parse the content length from the raw // header string directly. // // See https://github.com/kevinsawicki/http-request/blob/http-request-5.6/lib/src/main/java/com/github/kevinsawicki/http/HttpRequest.java#L2519-L2521 String contentLengthheader = req.header(HttpRequest.HEADER_CONTENT_LENGTH); // The server may not send us the "Content-Length" header in the // response, e.g. when the server is using chunked transfer encoding. if (contentLengthheader != null) { size = Long.parseLong(contentLengthheader); } } if (size > 0) { monitor.onProgressNotify(size, false); } } File tmp = DataManager.createTempFile(); // Log.d(DEBUG_TAG, "write to " + tmp.getAbsolutePath()); if (monitor == null) { req.receive(tmp); } else { req.bufferSize(MonitoredFileOutputStream.BUFFER_SIZE); req.receive(new MonitoredFileOutputStream(tmp, monitor)); } if (!tmp.renameTo(file)) { Log.w(DEBUG_TAG, "Rename file error"); return null; } return file; } catch (SeafException e) { throw e; } catch (UnsupportedEncodingException e) { throw SeafException.encodingException; } catch (IOException e) { e.printStackTrace(); throw SeafException.networkException; } catch (HttpRequestException e) { if (e.getCause() instanceof MonitorCancelledException) { // Log.d(DEBUG_TAG, "download is cancelled"); throw SeafException.userCancelledException; } else { throw getSeafExceptionFromHttpRequestException(e); } } } private File getBlockFromLink(String dlink, FileBlocks fileBlocks, String blkId, String localPath, long fileSize, ProgressMonitor monitor) throws SeafException { if (dlink == null) return null; try { HttpRequest req = prepareApiFileGetRequest(dlink); checkRequestResponseStatus(req, HttpURLConnection.HTTP_OK); if (monitor != null) { /*if (req.header(HttpRequest.HEADER_CONTENT_LENGTH) == null) { throw SeafException.illFormatException; } Long size = Long.parseLong(req.header(HttpRequest.HEADER_CONTENT_LENGTH));*/ if (req.contentLength() > 0) { monitor.onProgressNotify(fileSize, true); } } File block = new File(localPath); // Log.d(DEBUG_TAG, "write to " + block.getAbsolutePath()); if (monitor == null) { req.receive(block); } else { req.bufferSize(DataManager.BUFFER_SIZE); req.receive(new MonitoredFileOutputStream(fileBlocks, blkId, block, monitor)); } return block; } catch (SeafException e) { throw e; } catch (UnsupportedEncodingException e) { throw SeafException.encodingException; } catch (IOException e) { e.printStackTrace(); throw SeafException.networkException; } catch (HttpRequestException e) { if (e.getCause() instanceof MonitorCancelledException) { // Log.d(DEBUG_TAG, "download is cancelled"); throw SeafException.userCancelledException; } else { throw getSeafExceptionFromHttpRequestException(e); } } } /** * Get the latest version of the file from server * @param repoID * @param path * @param localPath * @param cachedFileID The file id of the local cached version * @param monitor * @return A two tuple of (fileID, file). If the local cached version is up to date, the returned file is null. */ public Pair<String, File> getFile(String repoID, String path, String localPath, String cachedFileID, ProgressMonitor monitor) throws SeafException { Pair<String, String> ret = getDownloadLink(repoID, path); String dlink = ret.first; String fileID = ret.second; if (fileID.equals(cachedFileID)) { // cache is valid // Log.d(DEBUG_TAG, String.format("file %s is cached", path)); return new Pair<String, File>(fileID, null); } else { /*Log.d(DEBUG_TAG, String.format("file %s will be downloaded from server, latest %s, local cache %s", path, fileID, cachedFileID != null ? cachedFileID : "null"));*/ File file = getFileFromLink(dlink, path, localPath, fileID, monitor); if (file != null) { return new Pair<String, File>(fileID, file); } else { throw SeafException.unknownException; } } } // set password for an encrypted repo public void setPassword(String repoID, String passwd) throws SeafException { try { HttpRequest req = prepareApiPostRequest("api2/repos/" + repoID + "/", true, null); req.form("password", passwd); checkRequestResponseStatus(req, HttpURLConnection.HTTP_OK); } catch (SeafException e) { Log.d(DEBUG_TAG, "Set Password err: " + e.getCode()); throw e; } catch (Exception e) { Log.d(DEBUG_TAG, "Exception in setPassword "); e.printStackTrace(); return; } } private String getUploadLink(String repoID, boolean update) throws SeafException { return getUploadLink(repoID, update, false); } private String getUploadLink(String repoID, boolean update, boolean byblock) throws SeafException { try { String apiPath; if (update) { if (byblock) { apiPath = "api2/repos/" + repoID + "/upload-"; } else { apiPath = "api2/repos/" + repoID + "/update-"; } } else { apiPath = "api2/repos/" + repoID + "/upload-"; } if (byblock) { apiPath = apiPath + "blks-link/"; } else { apiPath = apiPath + "link/"; } HttpRequest req = prepareApiGetRequest(apiPath); checkRequestResponseStatus(req, HttpURLConnection.HTTP_OK); String result = new String(req.bytes(), "UTF-8"); // should return "\"http://gonggeng.org:8082/...\"" or "\"https://gonggeng.org:8082/...\" if (result.startsWith("\"http")) { // remove the starting and trailing quote return result.substring(1, result.length() - 1); } else throw SeafException.unknownException; } catch (SeafException e) { Log.d(DEBUG_TAG, e.getCode() + e.getMessage()); throw e; } catch (Exception e) { String msg = e.getMessage(); if (msg != null) Log.d(DEBUG_TAG, msg); else Log.d(DEBUG_TAG, "get upload link error", e); throw SeafException.unknownException; } } /** * Upload or update a file * * @param repoID * @param dir * @param filePath * @param monitor * @param update * @return * @throws SeafException */ public String uploadFile(String repoID, String dir, String filePath, ProgressMonitor monitor, boolean update) throws SeafException { try { String url = getUploadLink(repoID, update); return uploadFileCommon(url, repoID, dir, filePath, monitor, update); } catch (SeafException e) { // do again String url = getUploadLink(repoID, update); return uploadFileCommon(url, repoID, dir, filePath, monitor, update); } } private static final String CRLF = "\r\n"; private static final String TWO_HYPENS = "--"; private static final String BOUNDARY = "----SeafileAndroidBound$_$"; /** * Upload a file to seafile httpserver */ private String uploadFileCommon(String link, String repoID, String dir, String filePath, ProgressMonitor monitor, boolean update) throws SeafException { try { File file = new File(filePath); if (!file.exists()) { throw new SeafException(SeafException.OTHER_EXCEPTION, "File not exists"); } HttpRequest req = HttpRequest.post(link, null, false).followRedirects(true).connectTimeout(CONNECTION_TIMEOUT); prepareHttpsCheck(req); /** * We have to set the content-length header, otherwise the whole * request would be buffered by android. So we have to format the * multipart form-data request ourselves in order to calculate the * content length. */ int totalLen = 0; byte[] dirParam = {}; byte[] targetFileParam = {}; StringBuilder builder; if (update) { // the "target_file" param is for update file api builder = new StringBuilder(); // line 1, ------SeafileAndroidBound$_$ builder.append(TWO_HYPENS + BOUNDARY + CRLF); // line 2 builder.append("Content-Disposition: form-data; name=\"target_file\"" + CRLF); // line 3, an empty line builder.append(CRLF); String targetFilePath = Utils.pathJoin(dir, file.getName()); // line 4 builder.append(targetFilePath + CRLF); targetFileParam = builder.toString().getBytes("UTF-8"); totalLen += targetFileParam.length; } else { // the "parent_dir" param is for upload file api builder = new StringBuilder(); // line 1, ------SeafileAndroidBound$_$ builder.append(TWO_HYPENS + BOUNDARY + CRLF); // line 2 builder.append("Content-Disposition: form-data; name=\"parent_dir\"" + CRLF); // line 3, an empty line builder.append(CRLF); // line 4 builder.append(dir + CRLF); dirParam = builder.toString().getBytes("UTF-8"); totalLen += dirParam.length; } // line 1 String l1 = TWO_HYPENS + BOUNDARY + CRLF; // line 2, String contentDisposition = "Content-Disposition: form-data; name=\"file\";filename=\"" + file.getName() + "\"" + CRLF; byte[] l2 = contentDisposition.getBytes("UTF-8"); // line 3 String l3 = "Content-Type: text/plain" + CRLF; // line 4 String l4 = CRLF; totalLen += l1.length() + l2.length + l3.length() + l4.length() + file.length() + 2; String end = TWO_HYPENS + BOUNDARY + TWO_HYPENS + CRLF; totalLen += end.length(); if (totalLen >= 0) req.contentLength(totalLen); req.header("Connection", "close"); req.header("Cache-Control", "no-cache"); req.header("Content-Type", "multipart/form-data;boundary=" + BOUNDARY); if (update) { req.send(targetFileParam); } else { req.send(dirParam); } req.send(l1); req.send(l2); req.send(l3); req.send(l4); if (monitor != null) { req.bufferSize(MonitoredFileInputStream.BUFFER_SIZE); req.send(new MonitoredFileInputStream(file, monitor)); } else { req.send(new FileInputStream(file)); } req.send(CRLF); req.send(end); checkRequestResponseStatus(req, HttpURLConnection.HTTP_OK); return new String(req.bytes(), "UTF-8"); } catch (IOException e) { throw SeafException.networkException; } catch (HttpRequestException e) { if (e.getCause() instanceof MonitorCancelledException) { Log.d(DEBUG_TAG, "upload is cancelled"); throw SeafException.userCancelledException; } else { throw getSeafExceptionFromHttpRequestException(e); } } } /** * Upload file blocks to server */ private String uploadBlocksCommon(String link, String repoID, String dir, String filePath, List<Block> blocks, ProgressMonitor monitor, boolean update) throws SeafException { try { File file = new File(filePath); if (!file.exists()) { throw new SeafException(SeafException.OTHER_EXCEPTION, "File not exists"); } HttpRequest req = HttpRequest.post(link, null, false).followRedirects(true).connectTimeout(CONNECTION_TIMEOUT); prepareHttpsCheck(req); /** * We have to set the content-length header, otherwise the whole * request would be buffered by android. So we have to format the * multipart form-data request ourselves in order to calculate the * content length. */ int totalLen = 0; byte[] dirParam = {}; StringBuilder updateBuilder = new StringBuilder(); if (update) { // line 1, ------SeafileAndroidBound$_$ updateBuilder.append(TWO_HYPENS + BOUNDARY + CRLF); // line 2 updateBuilder.append("Content-Disposition: form-data; name=\"replace\"" + CRLF); // line 3 updateBuilder.append(CRLF); // line 4 updateBuilder.append("1" + CRLF); dirParam = updateBuilder.toString().getBytes("UTF-8"); totalLen += dirParam.length; } StringBuilder parentDirBuilder = new StringBuilder(); // line 1, ------SeafileAndroidBound$_$ parentDirBuilder.append(TWO_HYPENS + BOUNDARY + CRLF); // line 2 parentDirBuilder.append("Content-Disposition: form-data; name=\"parent_dir\"" + CRLF); // line 3 parentDirBuilder.append(CRLF); // line 4 parentDirBuilder.append(dir + CRLF); totalLen += parentDirBuilder.toString().getBytes("UTF-8").length; StringBuilder fileNameBuilder = new StringBuilder(); // line 1, ------SeafileAndroidBound$_$ fileNameBuilder.append(TWO_HYPENS + BOUNDARY + CRLF); // line 2 fileNameBuilder.append("Content-Disposition: form-data; name=\"file_name\"" + CRLF); // line 3 fileNameBuilder.append(CRLF); // line 4 fileNameBuilder.append(file.getName() + CRLF); totalLen += fileNameBuilder.toString().getBytes("UTF-8").length; StringBuilder fileSizeBuilder = new StringBuilder(); // line 1, ------SeafileAndroidBound$_$ fileSizeBuilder.append(TWO_HYPENS + BOUNDARY + CRLF); // line 2 fileSizeBuilder.append("Content-Disposition: form-data; name=\"file_size\"" + CRLF); // line 3 fileSizeBuilder.append(CRLF); // line 4 fileSizeBuilder.append(file.length() + CRLF); totalLen += fileSizeBuilder.toString().getBytes("UTF-8").length; for (Block block : blocks) { // line 1 String l1 = TWO_HYPENS + BOUNDARY + CRLF; File blk = new File(block.path); // line 2 String contentDisposition = "Content-Disposition: form-data; name=\"file\";filename=\"" + blk.getName() + "\"" + CRLF; byte[] l2 = contentDisposition.getBytes("UTF-8"); // line 3 String l3 = "Content-Type: text/plain" + CRLF; // line 4 String l4 = CRLF; totalLen += l1.length() + l2.length + l3.length() + l4.length() + blk.length() + 2; } String end = TWO_HYPENS + BOUNDARY + TWO_HYPENS + CRLF; totalLen += end.getBytes().length; req.contentLength(totalLen); req.header("Connection", "Keep-Alive"); req.header("Cache-Control", "no-cache"); req.header("Content-Type", "multipart/form-data;boundary=" + BOUNDARY); if (update) { req.send(updateBuilder); Log.d(DEBUG_TAG, updateBuilder.toString()); } req.send(parentDirBuilder); req.send(fileNameBuilder); req.send(fileSizeBuilder); for (Block block : blocks) { // line 1 String l1 = TWO_HYPENS + BOUNDARY + CRLF; File blk = new File(block.path); // line 2 String contentDisposition = "Content-Disposition: form-data; name=\"file\";filename=\"" + blk.getName() + "\"" + CRLF; byte[] l2 = contentDisposition.getBytes("UTF-8"); // line 3 String l3 = "Content-Type: text/plain" + CRLF; // line 4 String l4 = CRLF; totalLen += l1.length() + l2.length + l3.length() + l4.length() + blk.length() + 2; StringBuilder chunkReq = new StringBuilder(); chunkReq.append(l1).append(contentDisposition).append(l3).append(l4); req.send(chunkReq); if (monitor != null) { req.bufferSize(MonitoredFileInputStream.BUFFER_SIZE); req.send(new MonitoredFileInputStream(blk, monitor)); } else { req.send(new FileInputStream(blk)); } req.send(CRLF); } req.send(end); checkRequestResponseStatus(req, HttpURLConnection.HTTP_OK); //result file_id "3f0da9a0709c5fb9f23957608dabef01becc3a8c" return new String(req.bytes(), "UTF-8").replaceAll("\"", ""); } catch (IOException e) { throw SeafException.networkException; } catch (HttpRequestException e) { if (e.getCause() instanceof MonitorCancelledException) { Log.d(DEBUG_TAG, "upload is cancelled"); throw SeafException.userCancelledException; } else { throw getSeafExceptionFromHttpRequestException(e); } } } public void createNewRepo(String repoName, String description, String password) throws SeafException { HttpRequest req = prepareApiPostRequest("api2/repos/", true, null); req.form("name", repoName); if (description.length() > 0) { req.form("desc", description); } if (password.length() > 0) { req.form("passwd", password); } checkRequestResponseStatus(req, HttpURLConnection.HTTP_OK); } public Pair<String, String> createNewDir(String repoID, String parentDir, String dirName) throws SeafException { HttpRequest req = null; try { String fullPath = Utils.pathJoin(parentDir, dirName); final String encodeUriComponent = encodeUriComponent(fullPath).replaceAll("\\+", "%20"); Map<String, Object> params = Maps.newHashMap(); params.put("p", encodeUriComponent); params.put("reloaddir", "true"); req = prepareApiPostRequest("api2/repos/" + repoID + "/dir/", true, params, false); req.form("operation", "mkdir"); checkRequestResponseStatus(req, HttpURLConnection.HTTP_OK); String newDirID = req.header("oid"); if (newDirID == null) { return null; } String content = new String(req.bytes(), "UTF-8"); if (content.length() == 0) { return null; } return new Pair<String, String>(newDirID, content); } catch (SeafException e) { throw e; } catch (UnsupportedEncodingException e) { throw SeafException.encodingException; } catch (HttpRequestException e) { throw getSeafExceptionFromHttpRequestException(e); } } public Pair<String, String> createNewFile(String repoID, String parentDir, String fileName) throws SeafException { try { String fullPath = Utils.pathJoin(parentDir, fileName); final String encodeUriComponent = encodeUriComponent(fullPath).replaceAll("\\+", "%20"); Map<String, Object> params = Maps.newHashMap(); params.put("p", encodeUriComponent); params.put("reloaddir", "true"); HttpRequest req = prepareApiPostRequest("api2/repos/" + repoID + "/file/", true, params, false); req.form("operation", "create"); checkRequestResponseStatus(req, HttpURLConnection.HTTP_OK); String newDirID = req.header("oid"); if (newDirID == null) { return null; } String content = new String(req.bytes(), "UTF-8"); if (content.length() == 0) { return null; } return new Pair<String, String>(newDirID, content); } catch (SeafException e) { throw e; } catch (UnsupportedEncodingException e) { throw SeafException.encodingException; } catch (HttpRequestException e) { throw getSeafExceptionFromHttpRequestException(e); } } /** * Wrap a FileInputStream in a upload task. We publish the progress of the upload during the process, and if we detect the task has been cancelled by the user, we throw a {@link MonitorCancelledException} to indicate such a situation. */ private class MonitoredFileInputStream extends InputStream { public static final int BUFFER_SIZE = 1024; private static final long PROGRESS_UPDATE_INTERVAL = 1000; private ProgressMonitor monitor; private InputStream src; private long bytesRead = 0; private long nextUpdate = System.currentTimeMillis() + PROGRESS_UPDATE_INTERVAL; public MonitoredFileInputStream(File file, ProgressMonitor monitor) throws IOException { this.src = new FileInputStream(file); this.monitor = monitor; } @Override public int read(byte[] buffer) throws IOException { int read = src.read(buffer); if (read != -1) { bytesRead += read; } checkMonitor(); return read; } @Override public int read() throws IOException { int ret = src.read(); if (ret != -1) { ++bytesRead; if (bytesRead % BUFFER_SIZE == 0) { checkMonitor(); } } return ret; } @Override public void close() throws IOException { src.close(); } private void checkMonitor() throws MonitorCancelledException { if (monitor.isCancelled() || Thread.currentThread().isInterrupted()) { throw new MonitorCancelledException(); } if (System.currentTimeMillis() > nextUpdate) { monitor.onProgressNotify(bytesRead, false); nextUpdate = System.currentTimeMillis() + PROGRESS_UPDATE_INTERVAL; } } } /** * Wrap a FileOutputStream in a download task. We publish the upload progress during the process, and if we detect the task has been cancelled by the user, we throw a {@link MonitorCancelledException} to indicate such a situation. */ private class MonitoredFileOutputStream extends OutputStream { public static final int BUFFER_SIZE = 4096; private static final long PROGRESS_UPDATE_INTERVAL = 500; private ProgressMonitor monitor; private OutputStream dst; private long bytesWritten = 0; private long nextUpdate = System.currentTimeMillis() + PROGRESS_UPDATE_INTERVAL; private FileBlocks fileBlocks; private String blockId; public MonitoredFileOutputStream(File file, ProgressMonitor monitor) throws IOException { this.dst = new FileOutputStream(file); this.monitor = monitor; } public MonitoredFileOutputStream(FileBlocks fileBlocks, String blockId, File file, ProgressMonitor monitor) throws IOException { this.dst = new FileOutputStream(file); this.monitor = monitor; if (fileBlocks != null) { this.fileBlocks = fileBlocks; this.blockId = blockId; } } @Override public void write(byte[] buffer, int off, int len) throws IOException { dst.write(buffer, off, len); bytesWritten += len; checkMonitor(); } @Override public void write(byte[] buffer) throws IOException { dst.write(buffer); bytesWritten += buffer.length; checkMonitor(); } @Override public void write(int b) throws IOException { dst.write(b); ++bytesWritten; if (bytesWritten % BUFFER_SIZE == 0) { checkMonitor(); } } @Override public void close() throws IOException { dst.close(); } private void checkMonitor() throws MonitorCancelledException { if (monitor.isCancelled() || Thread.currentThread().isInterrupted()) { throw new MonitorCancelledException(); } if (System.currentTimeMillis() > nextUpdate) { if (fileBlocks != null) { fileBlocks.getBlock(blockId).finished = bytesWritten; monitor.onProgressNotify(fileBlocks.getFinished(), false); } else { monitor.onProgressNotify(bytesWritten, false); } nextUpdate = System.currentTimeMillis() + PROGRESS_UPDATE_INTERVAL; } } } private class MonitorCancelledException extends IOException { private static final long serialVersionUID = -1170466989781746232L; @Override public String toString() { return "the upload/download task has been cancelled"; } } public void renameRepo(String repoID, String newName) throws SeafException { Map<String, Object> params = Maps.newHashMap(); params.put("op", "rename"); HttpRequest req = prepareApiPostRequest(String.format("api2/repos/%s/", repoID), true, params); req.form("repo_name", newName); checkRequestResponseStatus(req, HttpURLConnection.HTTP_OK); } public void deleteRepo(String repoID) throws SeafException { HttpRequest req = prepareApiDeleteRequest(String.format("api2/repos/%s/", repoID), null); checkRequestResponseStatus(req, HttpURLConnection.HTTP_OK); } public boolean deleteShareLink(String token) throws SeafException { try { HttpRequest req = prepareApiDeleteRequest(String.format("api/v2.1/share-links/%s/", token), null); checkRequestResponseStatus(req, HttpURLConnection.HTTP_OK); String result = new String(req.bytes(), "UTF-8"); if (result != null && Utils.parseJsonObject(result) != null) { JSONObject obj = Utils.parseJsonObject(result); return obj.getBoolean("success"); } else { throw SeafException.illFormatException; } } catch (UnsupportedEncodingException e) { e.printStackTrace(); } catch (JSONException e) { e.printStackTrace(); } return false; } public String getShareLink(String repoID, String path) throws SeafException { try { Map<String, Object> params = Maps.newHashMap(); params.put("repo_id", repoID); params.put("path", path); HttpRequest req = prepareApiGetRequest("api/v2.1/share-links", params); checkRequestResponseStatus(req, HttpURLConnection.HTTP_OK); String result = new String(req.bytes(), "UTF-8"); return result; } catch (SeafException e) { throw e; } catch (HttpRequestException e) { throw getSeafExceptionFromHttpRequestException(e); } catch (IOException e) { throw SeafException.networkException; } } public String getShareLink(String repoID, String path, String password, String days) throws SeafException { try { Map<String, Object> params = Maps.newHashMap(); HttpRequest req = prepareApiPostRequest("api/v2.1/share-links/", true, params, false); req.form("repo_id", repoID); req.form("path", path); if (!TextUtils.isEmpty(password)) { req.form("password", password); } if (!TextUtils.isEmpty(days)) { req.form("expire_days", days); } checkRequestResponseStatus(req, HttpURLConnection.HTTP_OK); String result = new String(req.bytes(), "UTF-8"); if (result != null && Utils.parseJsonObject(result) != null) { JSONObject obj = Utils.parseJsonObject(result); return obj.getString("link"); } else { throw SeafException.illFormatException; } } catch (SeafException e) { throw e; } catch (HttpRequestException e) { throw getSeafExceptionFromHttpRequestException(e); } catch (UnsupportedEncodingException e) { throw SeafException.illFormatException; } catch (JSONException e) { e.printStackTrace(); throw SeafException.illFormatException; } } public void completeRemoteWipe(String token) throws SeafException { try { HttpRequest req = prepareApiPostRequest("api2/device-wiped/", true, null); req.form("token", token); checkRequestResponseStatus(req, HttpURLConnection.HTTP_CREATED); } catch (SeafException e) { throw e; } catch (HttpRequestException e) { throw getSeafExceptionFromHttpRequestException(e); } } public void star(String repoID, String path) throws SeafException { try { HttpRequest req = prepareApiPostRequest("api2/starredfiles/", true, null); req.form("repo_id", repoID); req.form("p", path); checkRequestResponseStatus(req, HttpURLConnection.HTTP_CREATED); } catch (SeafException e) { throw e; } catch (HttpRequestException e) { throw getSeafExceptionFromHttpRequestException(e); } } public void unstar(String repoID, String path) throws SeafException { try { Map<String, Object> params = Maps.newHashMap(); params.put("repo_id", repoID); params.put("p", path); HttpRequest req = prepareApiDeleteRequest("api2/starredfiles/", params); checkRequestResponseStatus(req, HttpURLConnection.HTTP_OK); } catch (SeafException e) { throw e; } catch (HttpRequestException e) { throw getSeafExceptionFromHttpRequestException(e); } } public Pair<String, String> rename(String repoID, String path, String newName, boolean isdir) throws SeafException { try { Map<String, Object> params = Maps.newHashMap(); params.put("p", encodeUriComponent(path).replaceAll("\\+", "%20")); params.put("reloaddir", "true"); String suffix = isdir ? "/dir/" : "/file/"; HttpRequest req = prepareApiPostRequest("api2/repos/" + repoID + suffix, true, params); req.form("operation", "rename"); req.form("newname", newName); checkRequestResponseStatus(req, HttpURLConnection.HTTP_OK); String newDirID = req.header("oid"); if (newDirID == null) { return null; } String content = new String(req.bytes(), "UTF-8"); if (content.length() == 0) { return null; } return new Pair<String, String>(newDirID, content); } catch (SeafException e) { throw e; } catch (UnsupportedEncodingException e) { throw SeafException.encodingException; } catch (HttpRequestException e) { throw getSeafExceptionFromHttpRequestException(e); } } public Pair<String, String> delete(String repoID, String path, boolean isdir) throws SeafException { try { Map<String, Object> params = Maps.newHashMap(); params.put("p", encodeUriComponent(path).replaceAll("\\+", "%20")); params.put("reloaddir", "true"); String suffix = isdir ? "/dir/" : "/file/"; HttpRequest req = prepareApiDeleteRequest("api2/repos/" + repoID + suffix, params); checkRequestResponseStatus(req, HttpURLConnection.HTTP_OK); String newDirID = req.header("oid"); if (newDirID == null) { return null; } String content = new String(req.bytes(), "UTF-8"); if (content.length() == 0) { return null; } return new Pair<String, String>(newDirID, content); } catch (SeafException e) { throw e; } catch (UnsupportedEncodingException e) { throw SeafException.encodingException; } catch (HttpRequestException e) { throw getSeafExceptionFromHttpRequestException(e); } } /** * Copy a file or multiple files, multiple file/folder names should be seperated by a ":". * * @param srcRepoId the source repo id * @param srcDir the source folder in src_repo * @param srcFn list of file/folder names to copy. Multiple file/folder names can be seperated by ":" * @param dstRepoId the destination repo id * @param dstDir the destination folder in dst_repo * @throws SeafException */ public void copy(String srcRepoId, String srcDir, String srcFn, String dstRepoId, String dstDir) throws SeafException { try { Map<String, Object> params = Maps.newHashMap(); params.put("p", encodeUriComponent(srcDir).replaceAll("\\+", "%20")); HttpRequest req = prepareApiPostRequest("api2/repos/" + srcRepoId + "/fileops/copy/", true, params); req.form("dst_repo", dstRepoId); req.form("dst_dir", dstDir); req.form("file_names", srcFn); checkRequestResponseStatus(req, HttpURLConnection.HTTP_OK); } catch (SeafException e) { throw e; } catch (HttpRequestException e) { throw getSeafExceptionFromHttpRequestException(e); } catch (UnsupportedEncodingException e) { throw SeafException.encodingException; } } /** * Move multiple files * * @param srcRepoId the source repo id * @param srcDir the source folder in src_repo * @param srcFn list of file/folder names to move. Multiple file/folder names can be seperated by ":" * @param dstRepoId the destination repo id * @param dstDir the destination folder in dst_repo * @throws SeafException */ public void move(String srcRepoId, String srcDir, String srcFn, String dstRepoId, String dstDir) throws SeafException { try { Map<String, Object> params = Maps.newHashMap(); params.put("p", srcDir); HttpRequest req = prepareApiPostRequest("api2/repos/" + srcRepoId + "/fileops/move/", true, params); req.form("dst_repo", dstRepoId); req.form("dst_dir", dstDir); req.form("file_names", srcFn); checkRequestResponseStatus(req, HttpURLConnection.HTTP_OK); } catch (SeafException e) { throw e; } catch (HttpRequestException e) { throw getSeafExceptionFromHttpRequestException(e); } } /** * Move a single file * * @param srcRepoId the source repo id * @param srcPath the source file path * @param dstRepoId the destination repo id * @param dstDir the destination folder in dst_repo * @return * @throws SeafException */ public Pair<String, String> move(String srcRepoId, String srcPath, String dstRepoId, String dstDir) throws SeafException { try { Map<String, Object> params = Maps.newHashMap(); params.put("p", encodeUriComponent(srcPath).replaceAll("\\+", "%20")); params.put("reloaddir", "true"); String suffix = "/file/"; HttpRequest req = prepareApiPostRequest("api2/repos/" + srcRepoId + suffix, true, params); req.form("operation", "move"); req.form("dst_repo", dstRepoId); req.form("dst_dir", dstDir); checkRequestResponseStatus(req, HttpURLConnection.HTTP_OK); String newDirID = req.header("oid"); if (newDirID == null) { return null; } String content = new String(req.bytes(), "UTF-8"); if (content.length() == 0) { return null; } return new Pair<String, String>(newDirID, content); } catch (SeafException e) { throw e; } catch (UnsupportedEncodingException e) { throw SeafException.encodingException; } catch (HttpRequestException e) { throw getSeafExceptionFromHttpRequestException(e); } } private void checkRequestResponseStatus(HttpRequest req, int expectedStatusCode) throws SeafException { if (req.code() != expectedStatusCode) { Log.d(DEBUG_TAG, "HTTP request failed : " + req.url() + ", " + req.code() + ", " + req.message()); if (req.message() == null) { throw SeafException.networkException; } else if (req.code() == HttpURLConnection.HTTP_UNAUTHORIZED) { String wiped = req.header("X-Seafile-Wiped"); if (wiped != null) { throw SeafException.remoteWipedException; } else { throw new SeafException(req.code(), req.message()); } } else { throw new SeafException(req.code(), req.message()); } } else { // Log.v(DEBUG_TAG, "HTTP request ok : " + req.url()); } } private void checkRequestResponseStatus(HttpRequest req, int expectedStatusCode, boolean withAuthToken) throws SeafException { if (req.code() != expectedStatusCode) { Log.d(DEBUG_TAG, "HTTP request failed : " + req.url() + ", " + req.code() + ", " + req.message()); if (req.message() == null) { throw SeafException.networkException; } else if (req.header("X-Seafile-OTP") != null && req.header("X-Seafile-OTP").equals("required")) { if (withAuthToken) throw SeafException.twoFactorAuthTokenInvalid; else throw SeafException.twoFactorAuthTokenMissing; } else { throw new SeafException(req.code(), req.message()); } } else { // Log.v(DEBUG_TAG, "HTTP request ok : " + req.url()); } } private SeafException getSeafExceptionFromHttpRequestException(HttpRequestException e) { if (e.getCause() instanceof SSLHandshakeException) { return SeafException.sslException; } else { return SeafException.networkException; } } }