/*
* Copyright 2014 Loic Merckel
*
* Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except
* in compliance with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software distributed under the License
* is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
* or implied. See the License for the specific language governing permissions and limitations under
* the License.
*/
// see https://developers.google.com/drive/v2/reference/
// see https://developers.google.com/drive/web/search-parameters
package io.uploader.drive.drive;
import com.google.api.client.http.HttpResponse;
import com.google.api.client.http.InputStreamContent;
import com.google.api.services.drive.Drive;
import com.google.api.services.drive.Drive.Children;
import com.google.api.services.drive.Drive.Files;
import com.google.api.services.drive.Drive.Files.Insert;
import com.google.api.services.drive.model.ChildList;
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.common.base.Preconditions;
import io.uploader.drive.config.Configuration;
import io.uploader.drive.config.HasConfiguration;
import io.uploader.drive.drive.largefile.GDriveUpdater;
import io.uploader.drive.drive.largefile.GDriveUploader;
import io.uploader.drive.drive.media.CustomDriveApiProgressListener;
import io.uploader.drive.drive.media.CustomProgressListener;
import io.uploader.drive.drive.media.MediaHttpUploader;
import io.uploader.drive.util.FileUtils.InputStreamProgressFilter;
import java.io.BufferedInputStream;
import java.io.FileInputStream;
import java.io.IOException;
import java.nio.file.Paths;
import java.nio.file.attribute.BasicFileAttributes;
import java.util.Arrays;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
public class DriveUtils {
private static final Logger logger = LoggerFactory.getLogger(DriveUtils.class);
private DriveUtils() {
super();
throw new IllegalStateException();
}
public interface HasDescription {
public abstract String getDescription () ;
}
public interface HasId {
public abstract String getId () ;
}
public interface HasMimeType {
public abstract String getMimeType () ;
}
public static HasId newId (final String id) {
return new HasId () {
@Override
public String getId() {
return (id);
}} ;
}
public static HasId newId (final File file) {
return new HasId () {
@Override
public String getId() {
return ((file == null)?(null):(file.getId())) ;
}} ;
}
public static HasMimeType newMineType (final String mineType) {
return new HasMimeType () {
@Override
public String getMimeType() {
return (mineType);
}} ;
}
public static HasMimeType newMineType (final File file) {
return new HasMimeType () {
@Override
public String getMimeType() {
return ((file == null)?(null):(file.getMimeType())) ;
}} ;
}
private static final String mimeTypeDirectory = "application/vnd.google-apps.folder";
private static final long largeFileMinimumSize = 30 * 1024 * 1024 ;
public static File getFile(Drive service, HasId id) throws IOException {
Preconditions.checkNotNull(service) ;
Preconditions.checkNotNull(id) ;
if (org.apache.commons.lang3.StringUtils.isEmpty(id.getId())) {
throw new IllegalArgumentException () ;
}
return service.files().get(id.getId()).execute();
}
public static ChildList getChildren(Drive service, File file) throws IOException {
Preconditions.checkNotNull(file) ;
if (org.apache.commons.lang3.StringUtils.isEmpty(file.getId())) {
throw new IllegalArgumentException () ;
}
Children.List request = service.children().list(file.getId());
return request.execute();
}
public static boolean isDirectory(File file)
throws IOException {
return (file == null) ? (false) : (mimeTypeDirectory.equals(file.getMimeType())) ;
}
/**
* Insert new folder.
*
* @param service
* Drive API service instance.
* @param title
* Title of the folder to insert.
* @param description
* Description of the file to insert.
* @param parentId
* Optional parent folder's ID.
* @return Inserted file metadata if successful, {@code null} otherwise.
* @throws IOException
*/
public static File insertDirectory(Drive service, String title,
HasDescription description, HasId parentId)
throws IOException {
if (service == null
|| org.apache.commons.lang3.StringUtils.isEmpty(title)) {
throw new IllegalArgumentException();
}
File body = new File();
body.setTitle(title);
body.setDescription((description==null) ? (null) : (description.getDescription()));
body.setMimeType(mimeTypeDirectory);
// Set the parent folder.
if (parentId != null) {
if (org.apache.commons.lang3.StringUtils.isNotEmpty(parentId.getId())) {
body.setParents(Arrays.asList(new ParentReference().setId(parentId.getId())));
}
}
File file = service.files().insert(body).execute();
return file;
}
/**
* Permanently delete a file, skipping the trash.
*
* @param service
* Drive API service instance.
* @param fileId
* ID of the file to delete.
* @throws IOException
*/
public static void deleteFile(Drive service, String fileId) throws IOException {
if (service == null || org.apache.commons.lang3.StringUtils.isEmpty(fileId)) {
throw new IllegalArgumentException();
}
service.files().delete(fileId).execute();
}
/**
* Move a file to the trash.
*
* @param service
* Drive API service instance.
* @param fileId
* ID of the file to trash.
* @return The updated file if successful, {@code null} otherwise.
* @throws IOException
*/
public static File trashFile(Drive service, HasId fileId) throws IOException {
if (service == null || fileId == null || org.apache.commons.lang3.StringUtils.isEmpty(fileId.getId())) {
throw new IllegalArgumentException();
}
return service.files().trash(fileId.getId()).execute();
}
/**
* Find folders.
*
* @param service
* Drive API service instance.
* @param parentId
* Optional parent folder's ID.
* @param maxResults
* Optional maximum number of folders in the returned list.
* @return List of file metadatas.
* @throws IOException
*/
public static FileList findDirectories(Drive service,
HasId parentId, Integer maxResults)
throws IOException {
return findDirectoriesWithTitle (service, null, parentId, maxResults) ;
}
/**
* Find folders.
*
* @param service
* Drive API service instance.
* @param title
* Optional title of the folder.
* @param parentId
* Optional parent folder's ID.
* @param maxResults
* Optional maximum number of folders in the returned list.
* @return List of file metadatas.
* @throws IOException
*/
public static FileList findDirectoriesWithTitle(Drive service,
String title, HasId parentId, Integer maxResults)
throws IOException {
Files.List request = service.files().list();
if (maxResults != null && maxResults.intValue() > 0) {
request = request.setMaxResults(maxResults);
}
StringBuilder query = new StringBuilder();
if (org.apache.commons.lang3.StringUtils.isNotEmpty(title)) {
query.append("title = '");
query.append(escape(title));
query.append("' and ");
}
query.append("mimeType='");
query.append(mimeTypeDirectory);
query.append("' and trashed=false");
if (parentId != null && org.apache.commons.lang3.StringUtils.isNotEmpty(parentId.getId())) {
query.append(" and '");
query.append(escape(parentId.getId()));
query.append("' in parents");
} else {
query.append(" and '");
query.append("root");
query.append("' in parents");
}
logger.info("findDirectoriesWithTitle: " + query.toString()) ;
request = request.setQ(query.toString());
FileList files = request.execute();
return files;
}
private static String escape (String str) {
if (str == null) {
return null ;
}
return str.replace("'", "\\'") ; //.replace("%", "\\%") ;
}
/**
* Find files.
*
* @param service
* Drive API service instance.
* @param title
* Title of the file, including the extension.
* @param parentId
* Optional parent folder's ID.
* @param mimeType
* Optional MIME type of the file.
* @param maxResults
* Optional maximum number of files in the returned list.
* @return List of file metadatas.
* @throws IOException
*/
public static FileList findFilesWithTitleAndMineType(Drive service,
String title, HasId parentId, HasMimeType mineType, Integer maxResults)
throws IOException {
Files.List request = service.files().list();
if (maxResults != null && maxResults.intValue() > 0) {
request = request.setMaxResults(maxResults);
}
StringBuilder query = new StringBuilder();
query.append("title = '");
query.append(escape(title));
query.append("'");
if (mineType != null && org.apache.commons.lang3.StringUtils.isNotEmpty(mineType.getMimeType())) {
query.append(" and mimeType='");
query.append(mineType.getMimeType());
query.append("'");
}
query.append(" and trashed=false");
if (parentId != null && org.apache.commons.lang3.StringUtils.isNotEmpty(parentId.getId())) {
query.append(" and '");
query.append(escape(parentId.getId()));
query.append("' in parents");
} else {
query.append(" and '");
query.append("root");
query.append("' in parents");
}
logger.info("findFilesWithTitleAndMineType: " + query.toString()) ;
request = request.setQ(query.toString());
FileList files = request.execute();
return files;
}
/**
* Insert new file.
*
* @param service
* Drive API service instance.
* @param title
* Title of the file to insert, including the extension.
* @param description
* Description of the file to insert.
* @param parentId
* Optional parent folder's ID.
* @param mimeType
* MIME type of the file to insert.
* @param filename
* Filename of the file to insert.
* @return Inserted file metadata if successful, {@code null} otherwise.
* @throws IOException
*/
public static File insertFile(Drive service, String title,
HasDescription description, HasId parentId, HasMimeType mimeType,
String filename, InputStreamProgressFilter.StreamProgressCallback progressCallback) throws IOException {
return insertFile(Configuration.INSTANCE, service, title,
description, parentId, mimeType,
filename, progressCallback) ;
}
/**
* Insert new file.
*
* @param config
* Configuration object.
* @param service
* Drive API service instance.
* @param title
* Title of the file to insert, including the extension.
* @param description
* Description of the file to insert.
* @param parentId
* Optional parent folder's ID.
* @param mimeType
* MIME type of the file to insert.
* @param filename
* Filename of the file to insert.
* @return Inserted file metadata if successful, {@code null} otherwise.
* @throws IOException
*/
public static File insertFile(HasConfiguration config, Drive service, String title,
HasDescription description, HasId parentId, HasMimeType mimeType,
String filename, InputStreamProgressFilter.StreamProgressCallback progressCallback) throws IOException {
if (service == null
|| org.apache.commons.lang3.StringUtils.isEmpty(title)
|| org.apache.commons.lang3.StringUtils.isEmpty(filename)) {
throw new IllegalArgumentException();
}
final String type = (mimeType==null) ? (null) : (mimeType.getMimeType()) ;
// File's metadata.
File body = new File();
body.setTitle(title);
body.setDescription((description==null) ? (null) : (description.getDescription()));
body.setMimeType(type);
body.setOriginalFilename(filename);
// Set the parent folder.
if (parentId != null) {
if (org.apache.commons.lang3.StringUtils.isNotEmpty(parentId.getId())) {
body.setParents(Arrays.asList(new ParentReference().setId(parentId.getId())));
}
}
// https://code.google.com/p/google-api-java-client/wiki/MediaUpload
BasicFileAttributes attr = io.uploader.drive.util.FileUtils
.getFileAttr(Paths.get(filename));
boolean useMediaUpload = (attr != null && attr.size() > largeFileMinimumSize);
boolean useOldApi = true ;
boolean useCustomMediaUpload = true ;
if (useMediaUpload) {
File file = null ;
// if large file, there exists a nasty bug in the new API which remains unresolved,
// therefore we currently need to rely on the old API (even though it has been deprecated...)
// see: https://code.google.com/p/google-api-python-client/issues/detail?id=231
if (useOldApi) {
GDriveUploader upload = new GDriveUploader(config,
title, description, parentId, mimeType, filename,
progressCallback);
String fileId = upload.uploadFile();
Preconditions.checkState(org.apache.commons.lang3.StringUtils.isNotEmpty(fileId));
// get the file from response
file = getFile (service, newId(fileId)) ;
}
else
{
logger.info("Media Upload is used for large files");
java.io.File mediaFile = new java.io.File(filename);
InputStreamContent mediaContent = new InputStreamContent(type,
new BufferedInputStream(
io.uploader.drive.util.FileUtils
.getInputStreamWithProgressFilter(
progressCallback, mediaFile.length(),
new FileInputStream(mediaFile))));
mediaContent.setRetrySupported(true) ;
// TODO: there seems to exist a bug when the size is set... (java.io.IOException: insufficient data written)
// ...
//mediaContent.setLength(mediaFile.length());
mediaContent.setLength(-1);
Drive.Files.Insert request = service.files().insert(body, mediaContent);
if (useCustomMediaUpload) {
MediaHttpUploader mediaHttpUploader = new MediaHttpUploader (
mediaContent,
request.getAbstractGoogleClient().getRequestFactory().getTransport(),
request.getAbstractGoogleClient().getRequestFactory().getInitializer()) ;
mediaHttpUploader.setDisableGZipContent(true) ;
mediaHttpUploader.setProgressListener(new CustomProgressListener());
HttpResponse response = mediaHttpUploader.upload(request.buildHttpRequestUrl());
try {
if (!response.isSuccessStatusCode()) {
logger.error ("Error occurred while transferring the large file: " + response.getStatusMessage() + " (Status code: "+ response.getStatusCode()+ ")") ;
}
file = response.parseAs(com.google.api.services.drive.model.File.class) ;
} finally {
response.disconnect();
}
} else {
request.getMediaHttpUploader().setDisableGZipContent(true) ;
request.getMediaHttpUploader().setProgressListener(new CustomDriveApiProgressListener()) ;
request.setDisableGZipContent(true) ;
file = request.execute();
}
}
return file ;
} else {
// File's content.
java.io.File fileContent = new java.io.File(filename);
DriveFileContent mediaContent = new DriveFileContent(type,
fileContent, progressCallback);
Insert insert = service.files().insert(body, mediaContent) ;
return insert.execute();
}
}
/**
* Update an existing file's metadata and content.
*
* @param service
* Drive API service instance.
* @param fileId
* ID of the file to update.
* @param newTitle
* New title for the file.
* @param newDescription
* New description for the file.
* @param newMimeType
* New MIME type for the file.
* @return Updated file metadata if successful, {@code null} otherwise.
* @throws IOException
*/
public static File updateFile(Drive service, HasId fileId, String newTitle,
HasDescription newDescription, HasMimeType newMimeType,
String filename,
InputStreamProgressFilter.StreamProgressCallback progressCallback)
throws IOException {
return updateFile (Configuration.INSTANCE, service, fileId, newTitle,
newDescription, newMimeType, filename, progressCallback) ;
}
/**
* Update an existing file's metadata and content.
*
* @param config
* Configuration object.
* @param service
* Drive API service instance.
* @param fileId
* ID of the file to update.
* @param newTitle
* New title for the file.
* @param newDescription
* New description for the file.
* @param newMimeType
* New MIME type for the file.
* @return Updated file metadata if successful, {@code null} otherwise.
* @throws IOException
*/
public static File updateFile(HasConfiguration config, Drive service, HasId fileId, String newTitle,
HasDescription newDescription, HasMimeType newMimeType,
String filename,
InputStreamProgressFilter.StreamProgressCallback progressCallback)
throws IOException {
Preconditions.checkNotNull(fileId);
if (org.apache.commons.lang3.StringUtils.isEmpty(fileId.getId())) {
throw new IllegalArgumentException();
}
// First retrieve the file from the API.
File file = service.files().get(fileId.getId()).execute();
// File's new metadata.
if (org.apache.commons.lang3.StringUtils.isNotEmpty(newTitle)) {
file.setTitle(newTitle);
}
if (newDescription != null) {
file.setDescription(newDescription.getDescription());
}
if (newMimeType != null) {
file.setMimeType(newMimeType.getMimeType());
}
// if large file, the same bug as for uploading exists, therefore we currently need to rely on the old api
// see: https://code.google.com/p/google-api-python-client/issues/detail?id=231
boolean useOldApi = true ;
boolean useMediaUpload = false ;
DriveFileContent mediaContent = null ;
if (org.apache.commons.lang3.StringUtils.isNotEmpty(filename)) {
BasicFileAttributes attr = io.uploader.drive.util.FileUtils
.getFileAttr(Paths.get(filename));
useMediaUpload = (attr != null && attr.size() > largeFileMinimumSize);
java.io.File fileContent = new java.io.File(filename);
mediaContent = new DriveFileContent(file.getMimeType(), fileContent, progressCallback);
}
// Send the request to the API.
File updatedFile = null ;
if (useMediaUpload) {
// update metadata
logger.info("Update metadata");
updatedFile = service.files().update(fileId.getId(), file).execute();
// we need to upload the new media content
logger.info("Update content");
if (useOldApi) {
GDriveUpdater upload = new GDriveUpdater(config, newId(updatedFile.getId()), newMimeType, filename, progressCallback) ;
upload.updateFile();
} else {
// TODO:
// ...
throw new IllegalStateException ("Not implemented") ;
}
updatedFile = getFile (service, newId(updatedFile.getId())) ;
} else {
// update metadata, and content (if any) of small files
if (mediaContent != null) {
updatedFile = service.files().update(fileId.getId(), file, mediaContent).execute();
} else {
updatedFile = service.files().update(fileId.getId(), file).execute();
}
}
return updatedFile;
}
}