Google DriveService API Upload with mimetype application/vnd.google-apps.document failing - google-drive-api

I am trying to upload a file to Google Drive and share/access the same via Google Docs. I have copied below the code snippet. I am using Google Drive API V3. It's failing with error GoogleApiException:
Google.Apis.Requests.RequestError Bad Request [400].
Not sure what's missing in the request. I have tried few additional parameters of File, but it always returns Bad Request Error. If anyone know the solution please let me know.
string fileType = "application/vnd.google-apps.document";
var newFile = new File
{
Name = title,
MimeType = fileType,
Description= title,
};
var uploadProgress = DriveService.Files.Create(newFile, fileStream, fileType);
Google.Apis.Upload.IUploadProgress progress = uploadProgress.Upload();

I believe that in v3 you do not need to specify the mimeType as a third parameter for DriveService.Files.Create()
This is opposed to Files.Insert() in v2, where you would also need to specify Convert = true
Change to:
var uploadProgress = DriveService.Files.Create(newFile, fileStream);
Make sure that your original mimeType is compatible with Google Docs
Make sure that you build fileStream correctly.

Related

Why do I get an empty 200 OK response when uploading a file to a shared Google Drive folder when using a service account?

I am trying to use a Service Account to upload a file into a shared Google Drive folder, using Google Drive API v3 and the .Net client. The service account is added to the folder and "Can organise, add and edit". And if I do a list request I get the folder back (so I know the service account authentication works and that it at least has access to the shared folder).
If I try to upload a file though, I get a 200 response (so no error) but with an empty ResponseBody. And if I list again, the file isn't there. I don't understand what this means or why it is happening and can't find any information about this situation anywhere.
Here's my upload code. [PARENT_ID] is the ID of the shared folder and I've got the content of the CSV file I'm uploading and converted into bytes to put into the stream I've then sent.
using (var driveService = new DriveService(new BaseClientService.Initializer()
{
HttpClientInitializer = googleCredential.CreateScoped(DriveService.Scope.Drive),
ApplicationName = "FunctionApp"
}))
{
File fileMetadata = new File()
{
Name = "FileName",
MimeType = "application/vnd.google-apps.spreadsheet",
Parents = new List<string>() { "[PARENT_ID]" }
};
FilesResource.CreateMediaUpload request;
using (var csvStream = new MemoryStream())
{
csvStream.Write(csvBytes);
request = driveService.Files.Create(
fileMetadata, csvStream, "text/csv");
}
request.Fields = "id";
request.Upload();
var file = request.ResponseBody;
Console.WriteLine("File ID: " + file.Id);
}
The file variable is always null so obviously file.Id throws a NullReferenceException.
I've been staring at this for hours but have not been able to work out what is wrong. Any ideas?
I've tried saving the csv string to a file a then reading it in a file stream (closer to the example at the bottom of https://developers.google.com/drive/api/v3/manage-uploads#import_to_google_docs_types_) but this yields the same result.
Note: I've been able to create a file in the same folder using the "Try this API" tool on https://developers.google.com/drive/api/v3/reference/files/create using the parameters the same as what I pass in the metadata here, but of course that doesn't use a service account and doesn't pass any actual data to the file.
Thanks to #JonSkeet for the solution on GitHub (https://github.com/googleapis/google-api-dotnet-client/issues/1490)
The problem was that the stream was being disposed before the upload (ie after the using). The code change was to not use a using at all.
var csvStream = new MemoryStream(csvBytes);
request = driveService.Files.Create(fileMetadata, csvStream, "text/csv");
var result = request.Upload();
This did a successful upload as expected.
This also didn't use the request.Fields although this could maybe have been left in.

Google app scripts: email a spreadsheet as excel

How do you make an app script which attaches a spreadsheet as an excel file and emails it to a certain email address?
There are some older posts on Stackoverflow on how to do this however they seem to be outdated now and do not seem to work.
Thank you.
It looks like #Christiaan Westerbeek's answer is spot on but its been a year now since his post and I think there needs to be a bit of a modification in the script he has given above.
var url = file.exportLinks[MimeType.MICROSOFT_EXCEL];
There is something wrong with this line of code, maybe that exportLinks has now depreciated. When I executed his code it gave an error to the following effect:
TypeError: Cannot read property "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet" from undefined.
The workaround is as follows:
The URL in the above line of code is basically the "download as xlsx" URL that can be used to directly download the spreadsheet as an xlsx file that you get from File> Download as > Microsoft Excel (.xlsx)
This is the format:
https://docs.google.com/spreadsheets/d/<<<ID>>>/export?format=xlsx&id=<<<ID>>>
where <<>> should be replaced by the ID of your file.
Check here to easily understand how to extract the ID from the URL of your google sheet.
Here's an up-to-date and working version. One prerequisite for this Google Apps script to work is that the Drive API v2 Advanced Google Service must be enabled. Enable it in your Google Apps script via Resources -> Advanced Google Services... -> Drive API v2 -> on. Then, that window will tell you that you must also enabled this service in the Google Developers Console. Follow the link and enable the service there too! When you're done, just use this script.
/**
* Thanks to a few answers that helped me build this script
* Explaining the Advanced Drive Service must be enabled: http://stackoverflow.com/a/27281729/1385429
* Explaining how to convert to a blob: http://ctrlq.org/code/20009-convert-google-documents
* Explaining how to convert to zip and to send the email: http://ctrlq.org/code/19869-email-google-spreadsheets-pdf
* New way to set the url to download from by #tera
*/
function emailAsExcel(config) {
if (!config || !config.to || !config.subject || !config.body) {
throw new Error('Configure "to", "subject" and "body" in an object as the first parameter');
}
var spreadsheet = SpreadsheetApp.getActiveSpreadsheet();
var spreadsheetId = spreadsheet.getId()
var file = Drive.Files.get(spreadsheetId);
var url = 'https://docs.google.com/spreadsheets/d/'+spreadsheetId+'/export?format=xlsx';
var token = ScriptApp.getOAuthToken();
var response = UrlFetchApp.fetch(url, {
headers: {
'Authorization': 'Bearer ' + token
}
});
var fileName = (config.fileName || spreadsheet.getName()) + '.xlsx';
var blobs = [response.getBlob().setName(fileName)];
if (config.zip) {
blobs = [Utilities.zip(blobs).setName(fileName + '.zip')];
}
GmailApp.sendEmail(
config.to,
config.subject,
config.body,
{
attachments: blobs
}
);
}
Update: I updated the way to set the url to download from. Doing it through the file.exportLinks collection is not working anymore. Thanks to #tera for pointing that out in his answer.

Overwrite an Image File with Google Apps Script

Can I overwrite an image file with Google Apps Script? I've tried:
file.setContent(newBlobImage);
file.replace(newBlobImage);
Neither of those work. .setContent() will delete whatever data was in the file, and it looks like maybe it just writes the variable name as text, or something like that. I'm assuming that both .setContent() and .replace() are meant for text documents, and maybe that's why they don't work.
If it were a text file, or a spreadsheet, I might be able to clear it, then append new content.
I can trash the file, then create a new one, but I'd rather not if there is some other way.
If I write a file with the same name, it won't overwrite the existing file, it creates a another file with the same name.
The only way I've been able to trash the file is with DocsList and the only success I've had with creating an image file is with DriveApp. So I have to trash the file with DocsList, then create another file with DriveApp.
Well, I've figured out how to delete the file without sending it to the trash, so I won't need to clean out the trash later. The Google Drive SDK inside of Apps Script has a remove method that didn't send the file to trash, it's just gone.
var myFolder = DriveApp.getFolderById('3Bg2dKau456ySkhNBWB98W5sSTM');
thisFile = myFolder.getFilesByName(myFileName);
while (thisFile.hasNext()) {
var eachFile = thisFile.next();
var idToDLET = eachFile.getId();
Logger.log('idToDLET: ' + idToDLET);
var rtrnFromDLET = Drive.Files.remove(idToDLET);
};
So, I'm combining the DriveApp service and the DriveAPI to delete the file without sending it to the trash. The DriveAPI .remove needs the file ID, but I don't have the file ID, so the file gets looked up by name, then the file ID is retrieved, then the ID is used to delete the file. So, if I can't find a way to overwrite the file, I can at least delete the old file without it going to the trash.
I just noticed that the DriveAPI service has a Patch and an Update option.
.patch(resource, fileId, optionalArgs)
Google Documentation Patch Updates file metadata.
The resource arg is probably the metadata. The fileId is self explanatory. I'm guessing that the optionalArgs are parameters that follow the HTTP Request Patch semantics? I don't know.
It looks like both Patch and Update will update data. Update is a PUT request that will
clears previously set data if you don't supply optional parameters.
According to the documentation. So it's safer to use a Patch request because any parameters that are missing are simply ignored. I haven't tried it yet, but maybe this is the answer.
I'm getting an error with Patch, so I'll try Update:
.update(resource, fileId, mediaData)
That has a arg for mediaData in the form of a blob. And I think that is what I need. But I'm not sure what the resource parameter needs. So I'm stuck there.
An image file can be overwritten with Google Apps Script and the DriveAPI using the update() method:
.update(File resource, String fileId, Blob mediaData)
Where file resource is:
var myFileName = 'fileName' + '.jpg';
var file = {
title: myFileName,
mimeType: 'image/jpeg'
};
I'm getting the file ID with the DriveApp service, and the Blob is what was uploaded by the user.
In order to use DriveAPI, you need to add it through the Resources, Advanced Google Services menu. Set the Drive API to ON.
var allFilesByName,file,myFolder,myVar,theFileID,thisFile;
//Define var names without assigning a value
file = {
title: myFileName,
mimeType: 'image/jpeg'
};
myFolder = DriveApp.getFolderById('Folder ID');
allFilesByName = myFolder.getFilesByName(myFileName);
while (allFilesByName.hasNext()) {
thisFile = allFilesByName.next();
theFileID = thisFile.getId();
//Logger.log('theFileID: ' + theFileID);
myVar = Drive.Files.update(file, theFileID, uploadedBlob);
};
Thank you for this track !
This allowed me to find a solution to my problem : move a bound form after copying and moved his spreadsheet.
The Drive app advanced service must be activated in the "Resource Script Editor" to run this script.
function spreadsheetCopy() {
// Below is the file to be copied with a bound form.
var fileToCopy = DriveApp.getFileById("file_key"); // key is fileId
var saveFolder = DriveApp.getFolderById("folder_key"); // key is folderId
var currentFolder = "";
( fileToCopy.getParents().next() ) ? currentFolder = fileToCopy.getParents().next() : currentFolder = DriveApp.getRootFolder();
Logger.log(currentFolder)
var copyFile = fileToCopy.makeCopy(saveFolder),
copyName = copyFile.getName();
Utilities.sleep(30000);
moveFormCopy(currentFolder, saveFolder, copyName);
}
function moveFormCopy(currentFolder, saveFolder, copyName) {
var formsInFolder = currentFolder.getFilesByType(MimeType.GOOGLE_FORMS);
var form, copyForm, copyFormMimeType, copyFormName, copyFormId;
while ( formsInFolder.hasNext() ) {
form = formsInFolder.next();
if ( copyName === form.getName() ) {
copyForm = form;
copyFormMimeType = copyForm.getMimeType();
copyFormName = copyForm.getName();
copyFormId = copyForm.getId();
break;
}
};
var resource = {title: copyName, mimeType: copyFormMimeType};
Drive.Files.patch(resource, copyFormId, {addParents: saveFolder.getId(), removeParents: currentFolder.getId()})
}

Create new file in a folder with Apps Script using Google Advanced Drive service

There are four ways to create a new file:
DocsList - Shown as DocsList in the Main List. Built in to Apps Script.
DriveApp - Shown as Drive in the Main List. Built in to Apps Script.
Drive API - Also shown as Drive in the Main List. Must be added to Apps Script.
DocumentApp - Shown as Document in the Main List. Built in, but only creates a document file.
They are all called services. Drive API is called an advanced service. So, which one should you use? I don't know, it depends. This question is about the Drive API Advanced Service.
I don't want to use 2 or 3 of the services to get the job done. I'd like to just use one of them. But to decide which one to use, I need to know the capabilities and options of all of them. If the simplest and easiest one to use will do everything I want, then I'll use that.
If I can create a new file with Drive API, but then I need to use the DriveApp service to move the file I created with Drive API, to the folder I want it in, then using Drive API in that particular situation is pointless.
I can create a new file in my Google Drive from a Google Apps Script .gs code, but the file gets written to the main 'My Drive'. I want to write the file directly to a sub-folder. My current code is:
var fileNameSetA = 'someFile.jpg';
var uploadedBlobA = an image uploaded with a file picker;
var fileTestDrive = {
title: fileNameSetA,
mimeType: 'image/jpeg'
};
fileTestDrive = Drive.Files.insert(fileTestDrive, uploadedBlobA);
Even though the code works, I have no idea why the syntax is the way it is, and I can't find documentation that tells me why. I can find a list of properties:
The title: and mimeType: are Optional Properties as part of the Request Body. From the example, the Optional Properties are obviously put in a key:value paired object. So, is the syntax:
Drive.Files.insert(optional properties, content);
There are also Required query parameters of:
uploadType --> media, multipart, resumable
But I don't see any required uploadType parameter designated anywhere in the example code. So, I don't understand Google's documentation.
Google Documentation Insert
Is it possible to write directly to a specific drive with Google Advanced Drive service in a Apps Script .gs code file? How do I do it?
The easiest way to create a new file is to use DriveApp which comes with pure Google Apps Script:
var dir = DriveApp.getFolderById("{dir_id}");
var file = dir.createFile(name, content);
If you do not know exact directory's id you can get the folder by its name:
var dir = DriveApp.getFoldersByName(name).next();
The next() is there because getFoldersByName() returns collection of all directories whose names match given value.
Also check DriveApp docs: https://developers.google.com/apps-script/reference/drive/drive-app
Maybe this is a bit late, but by looking at the REST API docs, it shows that you can use Drive.Files.insert to insert into any folder. You simply have to add the folder's ID in the properties of the file you are inserting as such:
var file = {
title: 'myFile',
"parents": [{'id':folder.getId()}], //<--By setting this parent ID to the folder's ID, it creates this file in the correct folder.
mimeType: 'image/png'
};
Folder ID can be obtained from the shareable link using the Google Drive GUI or as shown here. (e.g. Use the Execute function on the right.)
Alternatively, you can access the folder by name by replacing the folder.getID() with Drive.getFoldersByName('name of folder').
This is helpful because Drive.Files.insert() accepts arguments while Drive.createFile() and Drive.createFolder() do not.
The documentation for INSERT for the Drive API is found at this link:
Drive API for INSERT
There is a section for Request body. One of the Optional Properties for Insert is parents[]. The brackets [] indicate that a list of parents can be designated. The documentation for parents[] states this:
Collection of parent folders which contain this file. Setting this
field will put the file in all of the provided folders. On insert, if
no folders are provided, the file will be placed in the default root
folder.
So, . . . using Insert in Drive API, . . . . CAN write a new file directly to a subfolder. It's possible.
Now, the nomenclature and syntax for the Google Drive SDK, HTTP request is different than what is inside of Apps Script.
The syntax for invoking the Drive API HTTP Request inside of a .gs file is one of the following three:
Drive.Files.insert(FILE resource)
Drive.Files.insert(FILE resource, BLOB mediaData)
Drive.Files.insert(FILE resource, BLOB mediaData, OBJECT optionalArgs)
The syntax shown in the list above is from the auto-complete drop down list inside the Apps Script code editor. If you type Drive.Files. a list of possible methods will appear. I can't find information about the syntax anywhere in the online documentation.
So, where does the parents[] optional property go? Well, it's not a Blob, so we can rule that out. It's either FILE resource, or OBJECT optionalArgs. optionalArgs indicates that it's an object, but FILE resource is actually also an object.
In the examples, the FILE resource is constructed as key:value pair object.
Uploading Files - Advanced Drive Service - Google Documentation
Direct Answer to Question
This summary from https://developers.google.com/apps-script/advanced/drive sums things up pretty well:
The advanced Drive service allows you to use the Google Drive web API
in Apps Script. Much like Apps Script's built-in Drive service, this
API allows scripts to create, find, and modify files and folders in
Google Drive. In most cases, the built-in service is easier to
use, but this advanced service provides a few extra features,
including access to custom file properties as well as revisions for
files and folders.
Like all advanced services in Apps Script, the advanced Drive
service uses the same objects, methods, and parameters as the public
API.
Essentially DriveApp is easier to use than Drive, but Drive gives you more functionality since it shares the same functionality of the public API. I was not able to see how to save a file to a Shared/Team drive using DriveApp, so I ended up using Drive. The pain came around lack of documentation for the Google Apps Script implementation of Drive.
Explanation of My Solution and Code Sample:
A specific implementation of saving a file to Google drive, but this will likely be useful for others. It took me a whole day to figure this out since the documentation and code examples for Google Apps scripts is severely lacking. My use case was for saving a JSON file to a shared Google Drive (Team Drive).
There are three parameters that I did not have at first and my files were not uploading. I am not sure if all are necessary. One was the "kind": "drive#parentReference" part of the parents metadata. The next was "teamDriveId": teamDriveId which is also in the metadata. The last parameter was "supportsAllDrives": true which I passed in the optional parameter location of Drive.Files.insert().
I found the API explorer on https://developers.google.com/drive/api/v2/reference/files/insert to be very useful in figuring out which parameters were needed and how they needed to be formatted. I basically edited values in the explorer till I got a network request that worked. I then pulled the parameters I used into my Google Apps script.
/**
* Creates a JSON file in the designated Google Drive location
* #param {String} jsonString - A JS string from the result of a JSON.stringify(jsObject)
* #param {String} filename - The filename. Be sure to include the .json extension
* #param {String} folderId - The ID of the Google Drive folder where the file will be created
* #param {String} teamDriveId - The ID of the team drive
* #return {void}
*/
function createJSONFileInDriveFolder(jsonString, filename, folderId, teamDriveId) {
var metadata = {
"title": filename,
"mimeType": "application/json",
"parents": [
{
"id": folderId,
"kind": "drive#parentReference"
}
],
"teamDriveId": teamDriveId
};
var optionalParams = {
"supportsAllDrives": true
};
try {
var jsonBlob = Utilities.newBlob(jsonString, 'application/vnd.google-apps.script+json');
Drive.Files.insert(metadata, jsonBlob, optionalParams);
} catch (error) {
Logger.log(error);
}
}
var searchthreads = GmailApp.search('in:inbox AND after:2020/11/30 AND has:attachment');//"in:all -in:trash category:social older_than:15d
Logger.log("GMAIL thread 0:"+ searchthreads[0].getId());
Logger.log("GMAIL thread 1:"+ searchthreads[1].getId());
Logger.log("GMAIL thread 2:"+ searchthreads[2].getId());
Logger.log("Active User: " + me);
Logger.log("Search Thread: " + searchthreads.length);
Logger.log("Gmail lenght" + gmailthread.length);
//Logger.log("Gmail lenght" + gmailMessages.length);
for (var i in searchthreads){
var messageCOunt = searchthreads[i].getMessageCount();
Logger.log("messageCOunt :" + messageCOunt);
var messages = searchthreads[i].getMessages();
for (var m in messages){
var messagesender = messages[m].getFrom();
var messageDate = messages[m].getDate();
var messageReplyTo = messages[m].getReplyTo();
var messagesubject = messages[m].getSubject();
var messagebody = messages[m].getSubject();
var messagephoneNo = messages[m].getSubject();
//messages[m].isInInbox();
var messageid = messages[m].getId();
var messageplainbody = messages[m].getSubject();//messages[0].getPlainBody();
var EmailStatus ='N';
var ApptStatus = "CVReceived";// Tracking till candidate offer and payout
var messageattachement = messages[m].getAttachments();
//var png=UrlFetchApp.fetch(messageattachement).getBlob();
//https://drive.google.com/drive/folders/1RY4i6FwUvfy5OxrJ1pZTxJAOxjFFXbhz?usp=sharing
var folder = DriveApp.getFolderById("1RY4i6FwUvfy5OxrJ1pZTxJAOxjFFXbhz");
// DriveApp.getFolderById("1RY4i6FwUvfy5OxrJ1pZTxJAOxjFFXbhz").createFile(png);
//DriveApp.createFile();
for (var k in messageattachement){
var filename = messageattachement[k].getName();
var filesize = messageattachement[k].getSize();
var filecontent = messageattachement[k].getContentType();
var fileBlob = messageattachement[k].getAs(filecontent);
var filecpblob = messageattachement[k].copyBlob();
//folder.createFile(filename, messageattachement);
var file = {
title: filename,
"parents": [{'id':folder.getId()}],
mimeType: filecontent
};
file = Drive.Files.insert(file, filecpblob);
//DataStudioApp
Logger.log('ID: %s, File size (bytes): %s', file.id, file.fileSize);
//folder.createFile(filecpblob);
}
var processeddate = new Date();
I know it's been a while since this question was posted. But here is the solution to help other readers. When using Drive.Files.insert() method, in order to specify a location for the inserted file, you must specify the parents[] property within the FILE resource. So expanding on #Alan Wells response here is the syntax for writing a blob as Goggle Spreadsheet format in a specific folder.
let newFile = {title: 'Title goes here', parents: [{id: targetFolderId}]};
let savedFile = Drive.Files.insert(
newFile,
blobGoesHere,
{mimeType: MimeType.GOOGLE_SHEETS, convert: true});
Please note that parents: takes an array of objects. You can specify multiple locations for a single file (it will be created in all the specified folders). Even if you want to use a single location you still have to provide this object in a list.
I was able to use the DriveApp to create a file in a specified folder this way.
var driveFolder = DriveApp.getFolderByName("MyDriveFolder");
var file = driveFolder.createFile(formObject.txtReceipt);
file.setName("MyFile");
PS: formObject.txtReceipt is coming from a file upload control on a form in the html and this returns a blob

Download a Google Docs Spreadsheet as HTML

Im using the Google Drive api with php to do all sorts of wonderful things but getting stuck with downloading a Spreadsheet in HTML format. I can use the getExportLinks() method to download it in its various formats, but all i want is the text from the sheet and not a formatted file in docx or ods.
This was possible with the google docs api by using the /feeds/download/spreadsheets/Export?exportFormat=html&format=html&key=ID url. But currently being redirected when I use it.
Is it possible to download a spreadsheet as html using the drive api?
#Andre J's answer works, the code:
Drive driveService = new Drive.Builder(TRANSPORT, JSON_FACTORY, credential).build();
File file = driveService.files().get(____documentKey____).execute();
String downloadUrl = file.getExportLinks().get("application/pdf");
downloadUrl = downloadUrl.replaceFirst("exportFormat=pdf", "exportFormat=html");
HttpResponse resp = driveService.getRequestFactory().buildGetRequest(new GenericUrl(downloadUrl))
.execute();
System.out.println("downloadUrl: " + downloadUrl);
InputStream fileContent = resp.getContent();
String htmlContent = new Scanner(fileContent).useDelimiter("\\A").next();
System.out.println("htmlContent: " + htmlContent);
Just change the exportformat in the exportLink to html.