package cloudsync.connector; import java.io.BufferedReader; import java.io.IOException; import java.io.InputStream; import java.io.InputStreamReader; import java.io.StringWriter; import java.net.UnknownHostException; import java.nio.charset.Charset; import java.nio.file.Files; import java.nio.file.Path; import java.nio.file.Paths; import java.nio.file.attribute.FileTime; import java.security.GeneralSecurityException; import java.text.DecimalFormat; import java.text.SimpleDateFormat; import java.util.ArrayList; import java.util.Collections; import java.util.Comparator; import java.util.Date; import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.logging.Level; import java.util.logging.Logger; import cloudsync.exceptions.FileIOException; import com.google.api.client.googleapis.json.GoogleJsonResponseException; import org.apache.commons.io.FileUtils; import org.apache.commons.lang3.StringUtils; import cloudsync.exceptions.CloudsyncException; import cloudsync.helper.CmdOptions; import cloudsync.helper.Handler; import cloudsync.helper.Helper; import cloudsync.model.options.NetworkErrorType; import cloudsync.model.Item; import cloudsync.model.ItemType; import cloudsync.model.RemoteItem; import cloudsync.model.LocalStreamData; import com.google.api.client.googleapis.auth.oauth2.GoogleAuthorizationCodeFlow; import com.google.api.client.googleapis.auth.oauth2.GoogleCredential; import com.google.api.client.googleapis.auth.oauth2.GoogleTokenResponse; import com.google.api.client.googleapis.media.MediaHttpUploader; import com.google.api.client.googleapis.media.MediaHttpUploader.UploadState; import com.google.api.client.googleapis.media.MediaHttpUploaderProgressListener; import com.google.api.client.http.GenericUrl; import com.google.api.client.http.HttpResponse; import com.google.api.client.http.HttpResponseException; import com.google.api.client.http.HttpTransport; import com.google.api.client.http.InputStreamContent; import com.google.api.client.http.javanet.NetHttpTransport; import com.google.api.client.json.JsonFactory; import com.google.api.client.json.JsonGenerator; import com.google.api.client.json.gson.GsonFactory; import com.google.api.client.json.jackson2.JacksonFactory; import com.google.api.services.drive.Drive; import com.google.api.services.drive.Drive.Files.Insert; import com.google.api.services.drive.Drive.Files.Update; import com.google.api.services.drive.DriveScopes; import com.google.api.services.drive.model.File; import com.google.api.services.drive.model.FileList; import com.google.api.services.drive.model.ParentReference; import com.google.api.services.drive.model.Property; public class RemoteGoogleDriveConnector implements RemoteConnector { private final static Logger LOGGER = Logger.getLogger(RemoteGoogleDriveConnector.class.getName()); private final static String SEPARATOR = "/"; private final static String REDIRECT_URL = "urn:ietf:wg:oauth:2.0:oob"; private final static String FOLDER = "application/vnd.google-apps.folder"; private final static String FILE = "application/octet-stream"; private final static int MIN_SEARCH_BREAK = 5000; private final static int MIN_SEARCH_RETRIES = 12; private final static int CHUNK_COUNT = 4; // * // 256kb private final static int MAX_RESULTS = 1000; private final static long MIN_TOKEN_REFRESH_TIMEOUT = 600; private GoogleTokenResponse clientToken; private GoogleCredential credential; private Drive service; private Path clientTokenPath; private Map<String, File> cacheFiles; private Map<String, File> cacheParents; private String basePath; private String backupName; private String historyName; private Integer historyCount; private long lastValidate = 0; private boolean showProgress; private NetworkErrorType networkErrorBehavior; private int retries; private int waitretry; private Charset charset; public RemoteGoogleDriveConnector() { } @Override public void init(String backupName, CmdOptions options) throws CloudsyncException { RemoteGoogleDriveOptions googleDriveOptions = new RemoteGoogleDriveOptions(options, backupName); Integer history = options.getHistory(); showProgress = options.showProgress(); retries = options.getRetries(); waitretry = options.getWaitRetry() * 1000; networkErrorBehavior = options.getNetworkErrorBehavior(); charset = options.getCharset(); cacheFiles = new HashMap<>(); cacheParents = new HashMap<>(); this.basePath = Helper.trim(googleDriveOptions.getClientBasePath(), SEPARATOR); this.backupName = backupName; this.historyCount = history; this.historyName = history > 0 ? backupName + " " + new SimpleDateFormat("yyyy.MM.dd_HH.mm.ss").format(new Date()) : null; final HttpTransport httpTransport = new NetHttpTransport(); final JsonFactory jsonFactory = new JacksonFactory(); if (StringUtils.isNotEmpty(googleDriveOptions.getServiceAccountUser())) { // Service Account auth - https://developers.google.com/api-client-library/java/google-api-java-client/oauth2#service_accounts try { credential = new GoogleCredential.Builder() .setTransport(httpTransport) .setJsonFactory(jsonFactory) .setServiceAccountId(googleDriveOptions.getServiceAccountEmail()) .setServiceAccountScopes(Collections.singletonList(DriveScopes.DRIVE)) .setServiceAccountUser(googleDriveOptions.getServiceAccountUser()) .setServiceAccountPrivateKeyFromP12File(new java.io.File(googleDriveOptions.getServiceAccountPrivateKeyP12Path())) .build(); } catch (GeneralSecurityException | IOException e) { throw new CloudsyncException("Can't init remote google drive connector", e); } } else { // Installed Applications auth - https://developers.google.com/api-client-library/java/google-api-java-client/oauth2#installed_applications final GoogleAuthorizationCodeFlow flow = new GoogleAuthorizationCodeFlow.Builder(httpTransport, jsonFactory, googleDriveOptions.getClientID(), googleDriveOptions.getClientSecret(), Collections.singletonList(DriveScopes.DRIVE)).setAccessType("offline").setApprovalPrompt("auto").build(); this.clientTokenPath = Paths.get(googleDriveOptions.getClientTokenPath()); try { final String clientTokenAsJson = Files.exists(this.clientTokenPath) ? FileUtils.readFileToString(this.clientTokenPath.toFile(), charset) : null; credential = new GoogleCredential.Builder().setTransport(new NetHttpTransport()).setJsonFactory(new GsonFactory()) .setClientSecrets(googleDriveOptions.getClientID(), googleDriveOptions.getClientSecret()).build(); if (StringUtils.isEmpty(clientTokenAsJson)) { final String url = flow.newAuthorizationUrl().setRedirectUri(REDIRECT_URL).build(); System.out.println("Please open the following URL in your browser, copy the authorization code and enter below."); System.out.println("\n" + url + "\n"); final String code = new BufferedReader(new InputStreamReader(System.in)).readLine().trim(); clientToken = flow.newTokenRequest(code).setRedirectUri(REDIRECT_URL).execute(); storeClientToken(jsonFactory); LOGGER.log(Level.INFO, "client token stored in '" + this.clientTokenPath + "'"); } else { clientToken = jsonFactory.createJsonParser(clientTokenAsJson).parse(GoogleTokenResponse.class); } credential.setFromTokenResponse(clientToken); } catch (final IOException e) { throw new CloudsyncException("Can't init remote google drive connector", e); } } } private void storeClientToken(final JsonFactory jsonFactory) throws IOException { final StringWriter jsonTrWriter = new StringWriter(); final JsonGenerator generator = jsonFactory.createJsonGenerator(jsonTrWriter); generator.serialize(clientToken); generator.flush(); generator.close(); FileUtils.writeStringToFile(clientTokenPath.toFile(), jsonTrWriter.toString(), charset); } @Override public void upload(final Handler handler, final Item item) throws CloudsyncException, FileIOException { initService(handler); String title = handler.getLocalProcessedTitle(item); File parentDriveItem = null; File driveItem; int retryCount = 0; do { try { refreshCredential(); parentDriveItem = _getDriveItem(item.getParent()); final ParentReference parentReference = new ParentReference(); parentReference.setId(parentDriveItem.getId()); driveItem = new File(); driveItem.setTitle(title); driveItem.setParents(Collections.singletonList(parentReference)); final LocalStreamData data = _prepareDriveItem(driveItem, item, handler, true); if (data == null) { driveItem = service.files().insert(driveItem).execute(); } else { final InputStreamContent params = new InputStreamContent(FILE, data.getStream()); params.setLength(data.getLength()); Insert inserter = service.files().insert(driveItem, params); MediaHttpUploader uploader = inserter.getMediaHttpUploader(); prepareUploader(uploader, data.getLength()); driveItem = inserter.execute(); } if (driveItem == null) { throw new CloudsyncException("Couldn't create item '" + item.getPath() + "'"); } _addToCache(driveItem, null); item.setRemoteIdentifier(driveItem.getId()); return; } catch (final IOException e) { if (parentDriveItem != null) { for (int i = 0; i < MIN_SEARCH_RETRIES; i++) { driveItem = _searchDriveItem(item.getParent(), title); if (driveItem != null) { LOGGER.log(Level.WARNING, "Google Drive IOException: " + getExceptionMessage(e) + " - found partially uploaded item - try to update"); item.setRemoteIdentifier(driveItem.getId()); update(handler, item, true); return; } LOGGER.log(Level.WARNING, "Google Drive IOException: " + getExceptionMessage(e) + " - item not uploaded - retry " + (i + 1) + "/" + MIN_SEARCH_RETRIES + " - wait " + MIN_SEARCH_BREAK + " ms"); sleep(MIN_SEARCH_BREAK); } } retryCount = validateException("remote upload", item, e, retryCount); if( retryCount == -1 ) // ignore a failing item (workaround for now) return; } } while (true); } @Override public void update(final Handler handler, final Item item, final boolean with_filedata) throws CloudsyncException, FileIOException { initService(handler); int retryCount = 0; do { try { refreshCredential(); if (item.isType(ItemType.FILE)) { final File _parentDriveItem = _getHistoryFolder(item); if (_parentDriveItem != null) { final File copyOfdriveItem = new File(); final ParentReference _parentReference = new ParentReference(); _parentReference.setId(_parentDriveItem.getId()); copyOfdriveItem.setParents(Collections.singletonList(_parentReference)); // copyOfdriveItem.setTitle(driveItem.getTitle()); // copyOfdriveItem.setMimeType(driveItem.getMimeType()); // copyOfdriveItem.setProperties(driveItem.getProperties()); final File _copyOfDriveItem = service.files().copy(item.getRemoteIdentifier(), copyOfdriveItem).execute(); if (_copyOfDriveItem == null) { throw new CloudsyncException("Couldn't make a history snapshot of item '" + item.getPath() + "'"); } } } File driveItem = new File(); final LocalStreamData data = _prepareDriveItem(driveItem, item, handler, with_filedata); if (data == null) { driveItem = service.files().update(item.getRemoteIdentifier(), driveItem).execute(); } else { final InputStreamContent params = new InputStreamContent(FILE, data.getStream()); params.setLength(data.getLength()); Update updater = service.files().update(item.getRemoteIdentifier(), driveItem, params); MediaHttpUploader uploader = updater.getMediaHttpUploader(); prepareUploader(uploader, data.getLength()); driveItem = updater.execute(); } if (driveItem == null) { throw new CloudsyncException("Couldn't update item '" + item.getPath() + "'"); } else if (driveItem.getLabels().getTrashed()) { throw new CloudsyncException("Remote item '" + item.getPath() + "' [" + driveItem.getId() + "] is trashed\ntry to run with --nocache"); } _addToCache(driveItem, null); return; } catch (final IOException e) { retryCount = validateException("remote update", item, e, retryCount); if(retryCount < 0) // TODO workaround - fix this later retryCount = 0; } } while (true); } @Override public void remove(final Handler handler, final Item item) throws CloudsyncException { initService(handler); int retryCount = 0; do { try { refreshCredential(); final File _parentDriveItem = _getHistoryFolder(item); if (_parentDriveItem != null) { final ParentReference parentReference = new ParentReference(); parentReference.setId(_parentDriveItem.getId()); File driveItem = new File(); driveItem.setParents(Collections.singletonList(parentReference)); driveItem = service.files().patch(item.getRemoteIdentifier(), driveItem).execute(); if (driveItem == null) { throw new CloudsyncException("Couldn't make a history snapshot of item '" + item.getPath() + "'"); } } else { service.files().delete(item.getRemoteIdentifier()).execute(); } _removeFromCache(item.getRemoteIdentifier()); return; } catch (final IOException e) { retryCount = validateException("remote remove", item, e, retryCount); if(retryCount < 0) // TODO workaround - fix this later retryCount = 0; } } while (true); } @Override public InputStream get(final Handler handler, final Item item) throws CloudsyncException { initService(handler); int retryCount = 0; do { try { refreshCredential(); final File driveItem = _getDriveItem(item); final String downloadUrl = driveItem.getDownloadUrl(); final HttpResponse resp = service.getRequestFactory().buildGetRequest(new GenericUrl(downloadUrl)).execute(); return resp.getContent(); } catch (final IOException e) { retryCount = validateException("remote get", item, e, retryCount); if(retryCount < 0) // TODO workaround - fix this later retryCount = 0; } } while (true); } @Override public List<RemoteItem> readFolder(final Handler handler, final Item parentItem) throws CloudsyncException { initService(handler); int retryCount = 0; do { try { refreshCredential(); final List<RemoteItem> child_items = new ArrayList<>(); final List<File> childDriveItems = _readFolder(parentItem.getRemoteIdentifier()); for (final File child : childDriveItems) { child_items.add(_prepareBackupItem(parentItem, child, handler)); } return child_items; } catch (final IOException e) { retryCount = validateException("remote fetch", parentItem, e, retryCount); if(retryCount < 0) // TODO workaround - fix this later retryCount = 0; } } while (true); } @Override public void cleanHistory(final Handler handler) throws CloudsyncException { initService(handler); final File backupDriveFolder = _getBackupFolder(); final File parentDriveItem = _getDriveFolder(basePath); try { refreshCredential(); final List<File> child_items = new ArrayList<>(); for (File file : _readFolder(parentDriveItem.getId())) { if (backupDriveFolder.getId().equals(file.getId()) || !file.getTitle().startsWith(backupDriveFolder.getTitle())) { continue; } child_items.add(file); } if (child_items.size() > historyCount) { Collections.sort(child_items, new Comparator<File>() { @Override public int compare(final File o1, final File o2) { final long v1 = o1.getCreatedDate().getValue(); final long v2 = o2.getCreatedDate().getValue(); if (v1 < v2) return 1; if (v1 > v2) return -1; return 0; } }); for (File file : child_items.subList(historyCount, child_items.size())) { LOGGER.log(Level.FINE, "cleanup history folder '" + file.getTitle() + "'"); service.files().delete(file.getId()).execute(); } } } catch (final IOException e) { throw new CloudsyncException("Unexpected error during history cleanup", e); } } private List<File> _readFolder(final String id) throws IOException { final List<File> child_items = new ArrayList<>(); final String q = "'" + id + "' in parents and trashed = false"; final Drive.Files.List request = service.files().list(); request.setQ(q); request.setMaxResults(MAX_RESULTS); do { FileList files = request.execute(); final List<File> result = files.getItems(); for (final File file : result) { child_items.add(file); } request.setPageToken(files.getNextPageToken()); } while (request.getPageToken() != null && request.getPageToken().length() > 0); return child_items; } private LocalStreamData _prepareDriveItem(final File driveItem, final Item item, final Handler handler, final boolean with_filedata) throws FileIOException { LocalStreamData data = null; if (with_filedata) { // "getLocalEncryptedBinary" should be called before "getMetadata" // to generate the needed checksum data = handler.getLocalProcessedBinary(item); } final String metadata = handler.getLocalProcessedMetadata(item); final List<Property> properties = new ArrayList<>(); final int length = metadata.length(); int partCounter = 0; // max 118 bytes (key+value) for (int i = 0; i < length; i += 100, partCounter++) { final String part = metadata.substring(i, Math.min(length, i + 100)); final Property property = new Property(); property.setKey("metadata" + partCounter); property.setValue(part); property.setVisibility("PRIVATE"); properties.add(property); } final Property property = new Property(); property.setKey("metadataParts"); property.setValue(Integer.toString(partCounter)); property.setVisibility("PRIVATE"); properties.add(property); driveItem.setProperties(properties); driveItem.setMimeType(item.isType(ItemType.FOLDER) ? FOLDER : FILE); return data; } private RemoteItem _prepareBackupItem(final Item parentItem, final File driveItem, final Handler handler) throws CloudsyncException { final List<Property> properties = driveItem.getProperties(); final Map<Integer, String> metadataMap = new HashMap<>(); int metadataPartCount = -1; if (properties != null) { for (final Property property : properties) { final String key = property.getKey(); if (!key.startsWith("metadata")) { continue; } if (key.equals("metadataParts")) { metadataPartCount = Integer.parseInt(property.getValue()); } else { metadataMap.put(Integer.parseInt(key.substring(8)), property.getValue()); } } } if (metadataPartCount == -1) metadataPartCount = metadataMap.size(); final List<String> parts = new ArrayList<>(); for (int i = 0; i < metadataPartCount; i++) { parts.add(i, metadataMap.get(i)); } try { String title = handler.getProcessedText(driveItem.getTitle()); String metadata = null; try { if (parts.size() > 0) { metadata = handler.getProcessedText(StringUtils.join(parts.toArray())); } else { ItemType type = driveItem.getMimeType().equals(FOLDER) ? ItemType.FOLDER : ItemType.FILE; LOGGER.log(Level.WARNING, "Found no metadata of " + type.getName() + " '" + parentItem.getPath() + "/" + title + "'"); } } catch (CloudsyncException e) { ItemType type = driveItem.getMimeType().equals(FOLDER) ? ItemType.FOLDER : ItemType.FILE; LOGGER.log(Level.WARNING, "Can't decrypt metadata of " + type.getName() + " '" + parentItem.getPath() + "/" + title + "'"); } return handler.initRemoteItem(driveItem.getId(), driveItem.getMimeType().equals(FOLDER), title, metadata, driveItem.getFileSize(), FileTime.fromMillis(driveItem.getCreatedDate().getValue())); } catch (CloudsyncException e) { ItemType type = driveItem.getMimeType().equals(FOLDER) ? ItemType.FOLDER : ItemType.FILE; throw new CloudsyncException("Can't decrypt name of " + type.getName() + " '" + driveItem.getId() + "' in '" + parentItem.getPath() + "/'"); } } private File _searchDriveItem(final Item parentItem, String title) throws CloudsyncException { int retryCount = 0; do { try { final String q = "title='" + title + "' and '" + parentItem.getRemoteIdentifier() + "' in parents and trashed = false"; final Drive.Files.List request = service.files().list(); request.setQ(q); final List<File> result = request.execute().getItems(); return result.size() == 0 ? null : result.get(0); } catch (final IOException e) { retryCount = validateException("remote search", parentItem, e, retryCount); if(retryCount < 0) // TODO workaround - fix this later retryCount = 0; } } while (true); } private File _getDriveItem(final Item item) throws CloudsyncException, IOException { final String id = item.getRemoteIdentifier(); if (cacheFiles.containsKey(id)) { return cacheFiles.get(id); } File driveItem; try { driveItem = service.files().get(id).execute(); } catch (HttpResponseException e) { if (e.getStatusCode() == 404) { throw new CloudsyncException("Couldn't find remote item '" + item.getPath() + "' [" + id + "]\ntry to run with --nocache"); } throw e; } if (driveItem.getLabels().getTrashed()) { throw new CloudsyncException("Remote item '" + item.getPath() + "' [" + id + "] is trashed\ntry to run with --nocache"); } _addToCache(driveItem, null); return driveItem; } private File _getHistoryFolder(final Item item) throws CloudsyncException, IOException { if (historyName == null) { return null; } final File driveRoot = _getBackupFolder(); final List<String> parentDriveTitles = new ArrayList<>(); Item parentItem = item; do { parentItem = parentItem.getParent(); if (parentItem.getRemoteIdentifier().equals(driveRoot.getId())) { break; } final File parentDriveItem = _getDriveItem(parentItem); parentDriveTitles.add(0, parentDriveItem.getTitle()); } while (true); return _getDriveFolder(basePath + SEPARATOR + historyName + SEPARATOR + StringUtils.join(parentDriveTitles, SEPARATOR)); } private File _getBackupFolder() throws CloudsyncException { return _getDriveFolder(basePath + SEPARATOR + backupName); } private File _getDriveFolder(final String path) throws CloudsyncException { int retryCount = 0; do { try { File parentItem = service.files().get("root").execute(); final String[] folderNames = StringUtils.split(path, SEPARATOR); for (final String name : folderNames) { if (cacheParents.containsKey(parentItem.getId() + ':' + name)) { parentItem = cacheParents.get(parentItem.getId() + ':' + name); } else { final String q = "title='" + name + "' and '" + parentItem.getId() + "' in parents and trashed = false"; final Drive.Files.List request = service.files().list(); request.setQ(q); request.setMaxResults(MAX_RESULTS); do { FileList files = request.execute(); final List<File> result = files.getItems(); // array('q' => q)) File _parentItem; if (result.size() == 0) { final File folder = new File(); folder.setTitle(name); folder.setMimeType(FOLDER); final ParentReference parentReference = new ParentReference(); parentReference.setId(parentItem.getId()); folder.setParents(Collections.singletonList(parentReference)); _parentItem = service.files().insert(folder).execute(); if (_parentItem == null) { throw new CloudsyncException("Couldn't create folder '" + name + "'"); } } else if (result.size() == 1) { _parentItem = result.get(0); } else { throw new CloudsyncException("base path '" + path + "' not unique"); } if (!_parentItem.getMimeType().equals(FOLDER)) { throw new CloudsyncException("No folder found at '" + path + "'"); } _addToCache(_parentItem, parentItem); parentItem = _parentItem; request.setPageToken(files.getNextPageToken()); } while (request.getPageToken() != null && request.getPageToken().length() > 0); } } return parentItem; } catch (final IOException e) { retryCount = validateException("remote get of '" + path + "'", null, e, retryCount); if(retryCount < 0) // TODO workaround - fix this later retryCount = 0; } } while (true); } private void _removeFromCache(final String id) { cacheFiles.remove(id); } private void _addToCache(final File driveItem, final File parentDriveItem) { if (driveItem.getMimeType().equals(FOLDER)) { cacheFiles.put(driveItem.getId(), driveItem); } if (parentDriveItem != null) { cacheParents.put(parentDriveItem.getId() + ':' + driveItem.getTitle(), driveItem); } } private void sleep(long duration) { try { Thread.sleep(duration); } catch (InterruptedException ex) { } } private int validateException(String name, Item item, IOException e, int count) throws CloudsyncException { if( e instanceof GoogleJsonResponseException) { StringBuilder info = new StringBuilder("Unexpected error during "); info.append(name); if (item != null) { info.append( " of " ); info.append( item.getInfo() ); } info.append(". Remote item not found.\ntry to run with --nocache"); switch( ((GoogleJsonResponseException)e).getStatusCode() ) { case 404: throw new CloudsyncException( info.toString(), e); } } if (count < retries) { long currentValidate = System.currentTimeMillis(); long current_retry_break = (currentValidate - lastValidate); if (lastValidate > 0 && current_retry_break < waitretry) { sleep(waitretry - current_retry_break); } lastValidate = currentValidate; count++; LOGGER.log(Level.WARNING, "Google Drive IOException: " + getExceptionMessage(e) + " - " + name + " - retry " + count + "/" + retries); return count; } if (e instanceof UnknownHostException) { LOGGER.log(Level.WARNING, "Connecting to RemoteHost " + getExceptionMessage(e) + " failed."); if( NetworkErrorType.ASK.equals(networkErrorBehavior) ) { String answer = null; while (answer == null || (!"Y".equals(answer) && !"n".equals(answer))) { System.out.print("Retry again (Y/n) "); try { answer = new BufferedReader(new InputStreamReader(System.in)).readLine().trim(); } catch (IOException _e) { break; } } if ("Y".equals(answer)) { lastValidate = System.currentTimeMillis(); return 0; } } else if( NetworkErrorType.CONTINUE.equals(networkErrorBehavior) ) { lastValidate = System.currentTimeMillis(); return -1; } } if (item != null) { throw new CloudsyncException("Unexpected error during " + name + " of " + item.getTypeName() + " '" + item.getPath() + "'", e); } else { throw new CloudsyncException("Unexpected error during " + name, e); } } private String getExceptionMessage(IOException e) { String msg = e.getMessage(); if (msg.contains("\n")) msg = msg.split("\n")[0]; return "'" + msg + "'"; } public void initService(Handler handler) throws CloudsyncException { if (service != null) return; final HttpTransport httpTransport = new NetHttpTransport(); final JsonFactory jsonFactory = new JacksonFactory(); service = new Drive.Builder(httpTransport, jsonFactory, null) .setApplicationName("Backup") .setHttpRequestInitializer(credential) .build(); if (StringUtils.isEmpty(credential.getServiceAccountId())) { credential.setExpiresInSeconds(MIN_TOKEN_REFRESH_TIMEOUT); } try { refreshCredential(); } catch (IOException e) { throw new CloudsyncException("couldn't refresh google drive token"); } handler.getRootItem().setRemoteIdentifier(_getBackupFolder().getId()); } private void refreshCredential() throws IOException { if (StringUtils.isNotEmpty(credential.getServiceAccountId())) return; if (credential.getExpiresInSeconds() > MIN_TOKEN_REFRESH_TIMEOUT) return; if (credential.refreshToken()) { clientToken.setAccessToken(credential.getAccessToken()); clientToken.setExpiresInSeconds(credential.getExpiresInSeconds()); clientToken.setRefreshToken(credential.getRefreshToken()); final JsonFactory jsonFactory = new JacksonFactory(); storeClientToken(jsonFactory); LOGGER.log(Level.INFO, "refreshed client token stored in '" + clientTokenPath + "'"); } } private void prepareUploader(MediaHttpUploader uploader, long length) { int chunkSize = MediaHttpUploader.MINIMUM_CHUNK_SIZE * CHUNK_COUNT; int chunkCount = (int) Math.ceil(length / (double) chunkSize); if (showProgress && chunkCount > 1) { uploader.setDirectUploadEnabled(false); uploader.setChunkSize(chunkSize); uploader.setProgressListener(new RemoteGoogleDriveProgress(this, length)); } else { uploader.setDirectUploadEnabled(true); } } private class RemoteGoogleDriveProgress implements MediaHttpUploaderProgressListener { private final long length; private final DecimalFormat df; private long lastBytes; private long lastTime; private final RemoteGoogleDriveConnector connector; public RemoteGoogleDriveProgress(RemoteGoogleDriveConnector connector, long length) { this.length = length; this.connector = connector; df = new DecimalFormat("00"); lastBytes = 0; lastTime = System.currentTimeMillis(); } @Override public void progressChanged(MediaHttpUploader mediaHttpUploader) throws IOException { if (mediaHttpUploader == null) return; switch ( mediaHttpUploader.getUploadState() ) { case INITIATION_COMPLETE: break; case INITIATION_STARTED: case MEDIA_IN_PROGRESS: this.connector.refreshCredential(); double percent = mediaHttpUploader.getProgress() * 100; long currentTime = System.currentTimeMillis(); String msg = "\r " + df.format(Math.ceil(percent)) + "% (" + convertToKB(mediaHttpUploader.getNumBytesUploaded()) + " of " + convertToKB(length) + " kb)"; if (mediaHttpUploader.getUploadState().equals(UploadState.MEDIA_IN_PROGRESS)) { long speed = convertToKB((mediaHttpUploader.getNumBytesUploaded() - lastBytes) / ((currentTime - lastTime) / 1000.0)); msg += " - " + speed + " kb/s"; } LOGGER.log(Level.FINEST, msg, true); lastTime = currentTime; lastBytes = mediaHttpUploader.getNumBytesUploaded(); break; case MEDIA_COMPLETE: // System.out.println("Upload is complete!"); default: break; } } private long convertToKB(double size) { return (long) Math.ceil(size / 1024); } } }